common.py 12.5 KB
Newer Older
1
# pylint: disable=missing-docstring
2
# pylint: disable=redefined-outer-name
3

4
import os
5
from lettuce import world, step
6
from nose.tools import assert_true, assert_in  # pylint: disable=no-name-in-module
Peter Fogg committed
7
from django.conf import settings
8

9
from student.roles import CourseStaffRole, CourseInstructorRole, GlobalStaff
10 11
from student.models import get_user

cahrens committed
12 13
from selenium.webdriver.common.keys import Keys

14
from logging import getLogger
15 16
from student.tests.factories import AdminFactory
from student import auth
17 18
logger = getLogger(__name__)

19 20
from terrain.browser import reset_data

Peter Fogg committed
21 22
TEST_ROOT = settings.COMMON_TEST_DATA_ROOT

23

24
@step('I (?:visit|access|open) the Studio homepage$')
Don Mitchell committed
25
def i_visit_the_studio_homepage(_step):
26 27 28
    # To make this go to port 8001, put
    # LETTUCE_SERVER_PORT = 8001
    # in your settings.py file.
29
    world.visit('/')
30
    signin_css = 'a.action-signin'
31
    assert world.is_css_present(signin_css)
32

33

34
@step('I am logged into Studio$')
Don Mitchell committed
35
def i_am_logged_into_studio(_step):
36 37
    log_into_studio()

38

39
@step('I confirm the alert$')
Don Mitchell committed
40
def i_confirm_with_ok(_step):
41 42
    world.browser.get_alert().accept()

43

44
@step(u'I press the "([^"]*)" delete icon$')
Don Mitchell committed
45
def i_press_the_category_delete_icon(_step, category):
46
    if category == 'section':
47
        css = 'a.action.delete-section-button'
48
    elif category == 'subsection':
49
        css = 'a.action.delete-subsection-button'
50 51
    else:
        assert False, 'Invalid category: %s' % category
52
    world.css_click(css)
53

54

cahrens committed
55
@step('I have opened a new course in Studio$')
Don Mitchell committed
56
def i_have_opened_a_new_course(_step):
57 58
    open_new_course()

59

60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79
@step('I have populated a new course in Studio$')
def i_have_populated_a_new_course(_step):
    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')
    world.wait_for_js_to_load()


80 81
@step('(I select|s?he selects) the new course')
def select_new_course(_step, whom):
82 83
    course_link_css = 'a.course-link'
    world.css_click(course_link_css)
84 85


86
@step(u'I press the "([^"]*)" notification button$')
87
def press_the_notification_button(_step, name):
88

89 90 91 92 93 94
    # Because the notification uses a CSS transition,
    # Selenium will always report it as being visible.
    # This makes it very difficult to successfully click
    # the "Save" button at the UI level.
    # Instead, we use JavaScript to reliably click
    # the button.
95
    btn_css = 'div#page-notification a.action-%s' % name.lower()
96 97
    world.trigger_event(btn_css, event='focus')
    world.browser.execute_script("$('{}').click()".format(btn_css))
98
    world.wait_for_ajax_complete()
99 100 101


@step('I change the "(.*)" field to "(.*)"$')
102
def i_change_field_to_value(_step, field, value):
103
    field_css = '#%s' % '-'.join([s.lower() for s in field.split()])
104 105 106
    ele = world.css_find(field_css).first
    ele.fill(value)
    ele._element.send_keys(Keys.ENTER)
107 108 109


@step('I reset the database')
110 111 112 113 114 115 116 117 118 119
def reset_the_db(_step):
    """
    When running Lettuce tests using examples (i.e. "Confirmation is
    shown on save" in course-settings.feature), the normal hooks
    aren't called between examples. reset_data should run before each
    scenario to flush the test database. When this doesn't happen we
    get errors due to trying to insert a non-unique entry. So instead,
    we delete the database manually. This has the effect of removing
    any users and courses that have been created during the test run.
    """
120 121
    reset_data(None)

122

123 124 125 126
@step('I see a confirmation that my changes have been saved')
def i_see_a_confirmation(step):
    confirmation_css = '#alert-confirmation'
    assert world.is_css_present(confirmation_css)
127 128


129
def open_new_course():
130
    world.clear_courses()
JonahStanley committed
131
    create_studio_user()
cahrens committed
132 133 134
    log_into_studio()
    create_a_course()

135

136 137
def create_studio_user(
        uname='robot',
138 139
        email='robot+studio@edx.org',
        password='test',
140
        is_staff=False):
141
    studio_user = world.UserFactory(
142
        username=uname,
143 144 145
        email=email,
        password=password,
        is_staff=is_staff)
146

147
    registration = world.RegistrationFactory(user=studio_user)
148 149 150
    registration.register(studio_user)
    registration.activate()

151 152
    return studio_user

153

154
def fill_in_course_info(
155 156
        name='Robot Super Course',
        org='MITx',
157
        num='101',
158
        run='2013_Spring'):
159 160 161
    world.css_fill('.new-course-name', name)
    world.css_fill('.new-course-org', org)
    world.css_fill('.new-course-number', num)
162
    world.css_fill('.new-course-run', run)
163

164

165 166 167
def log_into_studio(
        uname='robot',
        email='robot+studio@edx.org',
168 169
        password='test',
        name='Robot Studio'):
170

171
    world.log_in(username=uname, password=password, email=email, name=name)
172
    # Navigate to the studio dashboard
173
    world.visit('/')
Matjaz Gregoric committed
174
    assert_in(uname, world.css_text('span.account-username', timeout=10))
175

176

177 178 179 180 181
def add_course_author(user, course):
    """
    Add the user to the instructor group of the course
    so they will have the permissions to see it in studio
    """
182 183
    global_admin = AdminFactory()
    for role in (CourseStaffRole, CourseInstructorRole):
184
        auth.add_users(global_admin, role(course.id), user)
185 186


187
def create_a_course():
188 189 190 191 192
    course = world.CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course')
    world.scenario_dict['COURSE'] = course

    user = world.scenario_dict.get("USER")
    if not user:
193
        user = get_user('robot+studio@edx.org')
194

195
    add_course_author(user, course)
196 197 198

    # Navigate to the studio dashboard
    world.visit('/')
JonahStanley committed
199
    course_link_css = 'a.course-link'
200
    world.css_click(course_link_css)
201
    course_title_css = 'span.course-title'
202
    assert_true(world.is_css_present(course_title_css))
203

204

205
def add_section():
206 207
    world.css_click('.outline .button-new')
    assert_true(world.is_css_present('.outline-section .xblock-field-value'))
cahrens committed
208 209


210 211
def set_date_and_time(date_css, desired_date, time_css, desired_time, key=None):
    set_element_value(date_css, desired_date, key)
zubair-arbi committed
212
    world.wait_for_ajax_complete()
213

zubair-arbi committed
214
    set_element_value(time_css, desired_time, key)
215 216 217 218 219 220 221 222 223 224 225
    world.wait_for_ajax_complete()


def set_element_value(element_css, element_value, key=None):
    element = world.css_find(element_css).first
    element.fill(element_value)
    # hit TAB or provided key to trigger save content
    if key is not None:
        element._element.send_keys(getattr(Keys, key))  # pylint: disable=protected-access
    else:
        element._element.send_keys(Keys.TAB)  # pylint: disable=protected-access
226 227


228 229 230 231
@step('I have enabled the (.*) advanced module$')
def i_enabled_the_advanced_module(step, module):
    step.given('I have opened a new course section in Studio')
    world.css_click('.nav-course-settings')
232
    world.css_click('.nav-course-settings-advanced a')
233 234 235
    type_in_codemirror(0, '["%s"]' % module)
    press_the_notification_button(step, 'Save')

Jay Zoldak committed
236

237
@world.absorb
238
def create_unit_from_course_outline():
239
    """
240 241
    Expands the section and clicks on the New Unit link.
    The end result is the page where the user is editing the new unit.
242
    """
243
    css_selectors = [
244
        '.outline-subsection .expand-collapse', '.outline-subsection .button-new'
245 246 247 248 249 250
    ]
    for selector in css_selectors:
        world.css_click(selector)

    world.wait_for_mathjax()
    world.wait_for_xmodule()
251
    world.wait_for_loading()
252 253 254 255

    assert world.is_css_present('ul.new-component-type')


256 257 258 259 260 261 262 263
@world.absorb
def wait_for_loading():
    """
    Waits for the loading indicator to be hidden.
    """
    world.wait_for(lambda _driver: len(world.browser.find_by_css('div.ui-loading.is-hidden')) > 0)


264 265 266
@step('I have clicked the new unit button$')
@step(u'I am in Studio editing a new unit$')
def edit_new_unit(step):
267 268
    step.given('I have populated a new course in Studio')
    create_unit_from_course_outline()
269 270


cahrens committed
271
@step('the save notification button is disabled')
272 273 274
def save_button_disabled(step):
    button_css = '.action-save'
    disabled = 'is-disabled'
275
    assert world.css_has_class(button_css, disabled)
276 277


cahrens committed
278 279 280 281 282 283
@step('the "([^"]*)" button is disabled')
def button_disabled(step, value):
    button_css = 'input[value="%s"]' % value
    assert world.css_has_class(button_css, 'is-disabled')


284 285 286 287 288
def _do_studio_prompt_action(intent, action):
    """
    Wait for a studio prompt to appear and press the specified action button
    See cms/static/js/views/feedback_prompt.js for implementation
    """
289 290 291 292 293 294 295 296 297
    assert intent in [
        'warning',
        'error',
        'confirmation',
        'announcement',
        'step-required',
        'help',
        'mini',
    ]
298 299 300
    assert action in ['primary', 'secondary']

    world.wait_for_present('div.wrapper-prompt.is-shown#prompt-{}'.format(intent))
301

302 303 304 305 306 307
    action_css = 'li.nav-item > a.action-{}'.format(action)
    world.trigger_event(action_css, event='focus')
    world.browser.execute_script("$('{}').click()".format(action_css))

    world.wait_for_ajax_complete()
    world.wait_for_present('div.wrapper-prompt.is-hiding#prompt-{}'.format(intent))
308 309


310 311 312
@world.absorb
def confirm_studio_prompt():
    _do_studio_prompt_action('warning', 'primary')
313 314


315 316 317
@step('I confirm the prompt')
def confirm_the_prompt(step):
    confirm_studio_prompt()
318 319


320 321 322
@step(u'I am shown a prompt$')
def i_am_shown_a_notification(step):
    assert world.is_css_present('.wrapper-prompt')
323 324


Abdallah committed
325
def type_in_codemirror(index, text, find_prefix="$"):
326
    script = """
Abdallah committed
327
    var cm = {find_prefix}('div.CodeMirror:eq({index})').get(0).CodeMirror;
328 329
    cm.getInputField().focus();
    cm.setValue(arguments[0]);
Abdallah committed
330
    cm.getInputField().blur();""".format(index=index, find_prefix=find_prefix)
331
    world.browser.driver.execute_script(script, str(text))
332
    world.wait_for_ajax_complete()
Peter Fogg committed
333

Abdallah committed
334 335 336 337 338 339 340 341

def get_codemirror_value(index=0, find_prefix="$"):
    return world.browser.driver.execute_script(
        """
        return {find_prefix}('div.CodeMirror:eq({index})').get(0).CodeMirror.getValue();
        """.format(index=index, find_prefix=find_prefix)
    )

Peter Fogg committed
342

343 344
def attach_file(filename, sub_path):
    path = os.path.join(TEST_ROOT, sub_path, filename)
345
    world.browser.execute_script("$('input.file-input').css('display', 'block')")
346
    assert_true(os.path.exists(path))
347
    world.browser.attach_file('file', os.path.abspath(path))
348 349 350


def upload_file(filename, sub_path=''):
351
    # The file upload dialog is a faux modal, a div that takes over the display
352
    attach_file(filename, sub_path)
353 354
    modal_css = 'div.wrapper-modal-window-assetupload'
    button_css = '{} .action-upload'.format(modal_css)
Peter Fogg committed
355
    world.css_click(button_css)
356

357 358 359 360 361 362 363 364
    # Clicking the Upload button triggers an AJAX POST.
    world.wait_for_ajax_complete()

    # The modal stays up with a "File uploaded succeeded" confirmation message, then goes away.
    # It should take under 2 seconds, so wait up to 10.
    # Note that is_css_not_present will return as soon as the element is gone.
    assert world.is_css_not_present(modal_css, wait_time=10)

365 366 367 368 369 370 371 372 373 374 375 376

@step(u'"([^"]*)" logs in$')
def other_user_login(step, name):
    step.given('I log out')
    world.visit('/')

    signin_css = 'a.action-signin'
    world.is_css_present(signin_css)
    world.css_click(signin_css)

    def fill_login_form():
        login_form = world.browser.find_by_css('form#login_form')
cahrens committed
377 378
        login_form.find_by_name('email').fill(name + '@edx.org')
        login_form.find_by_name('password').fill("test")
379 380 381
        login_form.find_by_name('submit').click()
    world.retry_on_exception(fill_login_form)
    assert_true(world.is_css_present('.new-course-button'))
382
    world.scenario_dict['USER'] = get_user(name + '@edx.org')
383 384 385 386


@step(u'the user "([^"]*)" exists( as a course (admin|staff member|is_staff))?$')
def create_other_user(_step, name, has_extra_perms, role_name):
cahrens committed
387 388
    email = name + '@edx.org'
    user = create_studio_user(uname=name, password="test", email=email)
389 390
    if has_extra_perms:
        if role_name == "is_staff":
391
            GlobalStaff().add_users(user)
392 393 394
        else:
            if role_name == "admin":
                # admins get staff privileges, as well
395
                roles = (CourseStaffRole, CourseInstructorRole)
396
            else:
397
                roles = (CourseStaffRole,)
398
            course_key = world.scenario_dict["COURSE"].id
399
            global_admin = AdminFactory()
400
            for role in roles:
401
                auth.add_users(global_admin, role(course_key), user)
402 403 404 405 406


@step('I log out')
def log_out(_step):
    world.visit('logout')