Commit d38e69c6 by Braden MacDonald Committed by E. Kolpakov

Acceptance & Jasmine tests for library permissions editor

parent 93bb1f58
...@@ -256,6 +256,7 @@ define([ ...@@ -256,6 +256,7 @@ define([
"js/spec/views/pages/course_outline_spec", "js/spec/views/pages/course_outline_spec",
"js/spec/views/pages/course_rerun_spec", "js/spec/views/pages/course_rerun_spec",
"js/spec/views/pages/index_spec", "js/spec/views/pages/index_spec",
"js/spec/views/pages/library_users_spec",
"js/spec/views/modals/base_modal_spec", "js/spec/views/modals/base_modal_spec",
"js/spec/views/modals/edit_xblock_spec", "js/spec/views/modals/edit_xblock_spec",
......
define([
"jquery", "js/common_helpers/ajax_helpers", "js/spec_helpers/view_helpers",
"js/factories/manage_users_lib", "js/views/utils/view_utils"
],
function ($, AjaxHelpers, ViewHelpers, ManageUsersFactory, ViewUtils) {
"use strict";
describe("Library Instructor Access Page", function () {
var mockHTML = readFixtures('mock/mock-manage-users-lib.underscore');
beforeEach(function () {
ViewHelpers.installMockAnalytics();
appendSetFixtures(mockHTML);
ManageUsersFactory(
"Mock Library",
["honor@example.com", "audit@example.com", "staff@example.com"],
"dummy_change_role_url"
);
});
afterEach(function () {
ViewHelpers.removeMockAnalytics();
});
it("can give a user permission to use the library", function () {
var requests = AjaxHelpers.requests(this);
var reloadSpy = spyOn(ViewUtils, 'reload');
$('.create-user-button').click();
expect($('.wrapper-create-user')).toHaveClass('is-shown');
$('.user-email-input').val('other@example.com');
$('.form-create.create-user .action-primary').click();
AjaxHelpers.expectJsonRequest(requests, 'POST', 'dummy_change_role_url', {role: 'library_user'});
AjaxHelpers.respondWithJson(requests, {'result': 'ok'});
expect(reloadSpy).toHaveBeenCalled();
});
it("can cancel adding a user to the library", function () {
$('.create-user-button').click();
$('.form-create.create-user .action-secondary').click();
expect($('.wrapper-create-user')).not.toHaveClass('is-shown');
});
it("displays an error when the required field is blank", function () {
var requests = AjaxHelpers.requests(this);
$('.create-user-button').click();
$('.user-email-input').val('');
var errorPromptSelector = '.wrapper-prompt.is-shown .prompt.error';
expect($(errorPromptSelector).length).toEqual(0);
$('.form-create.create-user .action-primary').click();
expect($(errorPromptSelector).length).toEqual(1);
expect($(errorPromptSelector)).toContainText('You must enter a valid email address');
expect(requests.length).toEqual(0);
});
it("displays an error when the user has already been added", function () {
var requests = AjaxHelpers.requests(this);
$('.create-user-button').click();
$('.user-email-input').val('honor@example.com');
var warningPromptSelector = '.wrapper-prompt.is-shown .prompt.warning';
expect($(warningPromptSelector).length).toEqual(0);
$('.form-create.create-user .action-primary').click();
expect($(warningPromptSelector).length).toEqual(1);
expect($(warningPromptSelector)).toContainText('Already a library team member');
expect(requests.length).toEqual(0);
});
it("can remove a user's permission to access the library", function () {
var requests = AjaxHelpers.requests(this);
var reloadSpy = spyOn(ViewUtils, 'reload');
$('.user-item[data-email="honor@example.com"] .action-delete .delete').click();
expect($('.wrapper-prompt.is-shown .prompt.warning').length).toEqual(1);
$('.wrapper-prompt.is-shown .action-primary').click();
AjaxHelpers.expectJsonRequest(requests, 'DELETE', 'dummy_change_role_url', {role: null});
AjaxHelpers.respondWithJson(requests, {'result': 'ok'});
expect(reloadSpy).toHaveBeenCalled();
});
});
});
<div class="wrapper-mast wrapper">
<header class="mast has-actions has-subtitle">
<h1 class="page-header">
<small class="subtitle">Settings</small>
<span class="sr">&gt; </span>Instructor Access
</h1>
<nav class="nav-actions">
<h3 class="sr">Page Actions</h3>
<ul>
<li class="nav-item">
<a href="#" class="button new-button create-user-button"><i class="icon-plus"></i> Add Instructor</a>
</li>
</ul>
</nav>
</header>
</div>
<div class="wrapper-content wrapper">
<section class="content">
<article class="content-primary" role="main">
<div class="wrapper-create-element animate wrapper-create-user">
<form class="form-create create-user" id="create-user-form" name="create-user-form">
<div class="wrapper-form">
<h3 class="title">Grant Instructor Access to This Library</h3>
<fieldset class="form-fields">
<legend class="sr">New Instructor Information</legend>
<ol class="list-input">
<li class="field text required create-user-email">
<label for="user-email-input">User's Email Address</label>
<input id="user-email-input" class="user-email-input" name="user-email" type="text" placeholder="example: username@domain.com" value="">
<span class="tip tip-stacked">Please provide the email address of the instructor you'd like to add</span>
</li>
</ol>
</fieldset>
</div>
<div class="actions">
<button class="action action-primary" type="submit">Add User</button>
<button class="action action-secondary action-cancel">Cancel</button>
</div>
</form>
</div>
<ol class="user-list">
<li class="user-item" data-email="honor@example.com">
<span class="wrapper-ui-badge">
<span class="flag flag-role flag-role-staff is-hanging">
<span class="label sr">Current Role:</span>
<span class="value">
Staff
</span>
</span>
</span>
<div class="item-metadata">
<h3 class="user-name">
<span class="user-username">honor</span>
<span class="user-email">
<a class="action action-email" href="mailto:honor@example.com" title="send an email message to honor@example.com">honor@example.com</a>
</span>
</h3>
</div>
<ul class="item-actions user-actions">
<li class="action action-role">
<a href="#" class="make-instructor admin-role add-admin-role">Add Admin Access</span></a>
<a href="#" class="make-user admin-role remove-admin-role">Remove Staff Access</span></a>
</li>
<li class="action action-delete ">
<a href="#" class="delete remove-user action-icon" data-tooltip="Remove this user"><i class="icon-trash"></i><span class="sr">Delete the user, honor</span></a>
</li>
</ul>
</li>
<li class="user-item" data-email="audit@example.com">
<span class="wrapper-ui-badge">
<span class="flag flag-role flag-role-admin is-hanging">
<span class="label sr">Current Role:</span>
<span class="value">
Admin
</span>
</span>
</span>
<div class="item-metadata">
<h3 class="user-name">
<span class="user-username">audit</span>
<span class="user-email">
<a class="action action-email" href="mailto:audit@example.com" title="send an email message to audit@example.com">audit@example.com</a>
</span>
</h3>
</div>
<ul class="item-actions user-actions">
<li class="action action-role">
<a href="#" class="make-staff admin-role remove-admin-role">Remove Admin Access</span></a>
</li>
<li class="action action-delete ">
<a href="#" class="delete remove-user action-icon" data-tooltip="Remove this user"><i class="icon-trash"></i><span class="sr">Delete the user, audit</span></a>
</li>
</ul>
</li>
<li class="user-item" data-email="staff@example.com">
<span class="wrapper-ui-badge">
<span class="flag flag-role flag-role-user is-hanging">
<span class="label sr">Current Role:</span>
<span class="value">
User
</span>
</span>
</span>
<div class="item-metadata">
<h3 class="user-name">
<span class="user-username">staff</span>
<span class="user-email">
<a class="action action-email" href="mailto:staff@example.com" title="send an email message to staff@example.com">staff@example.com</a>
</span>
</h3>
</div>
<ul class="item-actions user-actions">
<li class="action action-role">
<a href="#" class="make-staff admin-role add-admin-role">Add Staff Access</span></a>
</li>
<li class="action action-delete ">
<a href="#" class="delete remove-user action-icon" data-tooltip="Remove this user"><i class="icon-trash"></i><span class="sr">Delete the user, staff</span></a>
</li>
</ul>
</li>
</ol>
</article>
</section>
</div>
...@@ -1746,6 +1746,7 @@ def auto_auth(request): ...@@ -1746,6 +1746,7 @@ def auto_auth(request):
* `staff`: Set to "true" to make the user global staff. * `staff`: Set to "true" to make the user global staff.
* `course_id`: Enroll the student in the course with `course_id` * `course_id`: Enroll the student in the course with `course_id`
* `roles`: Comma-separated list of roles to grant the student in the course with `course_id` * `roles`: Comma-separated list of roles to grant the student in the course with `course_id`
* `no_login`: Define this to create the user but not login
If username, email, or password are not provided, use If username, email, or password are not provided, use
randomly generated credentials. randomly generated credentials.
...@@ -1765,6 +1766,7 @@ def auto_auth(request): ...@@ -1765,6 +1766,7 @@ def auto_auth(request):
if course_id: if course_id:
course_key = CourseLocator.from_string(course_id) course_key = CourseLocator.from_string(course_id)
role_names = [v.strip() for v in request.GET.get('roles', '').split(',') if v.strip()] role_names = [v.strip() for v in request.GET.get('roles', '').split(',') if v.strip()]
login_when_done = 'no_login' not in request.GET
# Get or create the user object # Get or create the user object
post_data = { post_data = {
...@@ -1808,6 +1810,7 @@ def auto_auth(request): ...@@ -1808,6 +1810,7 @@ def auto_auth(request):
user.roles.add(role) user.roles.add(role)
# Log in as the user # Log in as the user
if login_when_done:
user = authenticate(username=username, password=password) user = authenticate(username=username, password=password)
login(request, user) login(request, user)
...@@ -1815,7 +1818,8 @@ def auto_auth(request): ...@@ -1815,7 +1818,8 @@ def auto_auth(request):
# Provide the user with a valid CSRF token # Provide the user with a valid CSRF token
# then return a 200 response # then return a 200 response
success_msg = u"Logged in user {0} ({1}) with password {2} and user_id {3}".format( success_msg = u"{} user {} ({}) with password {} and user_id {}".format(
u"Logged in" if login_when_done else "Created",
username, email, password, user.id username, email, password, user.id
) )
response = HttpResponse(success_msg) response = HttpResponse(success_msg)
......
...@@ -15,7 +15,7 @@ class AutoAuthPage(PageObject): ...@@ -15,7 +15,7 @@ class AutoAuthPage(PageObject):
this url will create a user and log them in. this url will create a user and log them in.
""" """
def __init__(self, browser, username=None, email=None, password=None, staff=None, course_id=None, roles=None): def __init__(self, browser, username=None, email=None, password=None, staff=None, course_id=None, roles=None, no_login=None):
""" """
Auto-auth is an end-point for HTTP GET requests. Auto-auth is an end-point for HTTP GET requests.
By default, it will create accounts with random user credentials, By default, it will create accounts with random user credentials,
...@@ -51,6 +51,9 @@ class AutoAuthPage(PageObject): ...@@ -51,6 +51,9 @@ class AutoAuthPage(PageObject):
if roles is not None: if roles is not None:
self._params['roles'] = roles self._params['roles'] = roles
if no_login:
self._params['no_login'] = True
@property @property
def url(self): def url(self):
""" """
...@@ -66,7 +69,7 @@ class AutoAuthPage(PageObject): ...@@ -66,7 +69,7 @@ class AutoAuthPage(PageObject):
def is_browser_on_page(self): def is_browser_on_page(self):
message = self.q(css='BODY').text[0] message = self.q(css='BODY').text[0]
match = re.search(r'Logged in user ([^$]+) with password ([^$]+) and user_id ([^$]+)$', message) match = re.search(r'(Logged in|Created) user ([^$]+) with password ([^$]+) and user_id ([^$]+)$', message)
return True if match else False return True if match else False
def get_user_id(self): def get_user_id(self):
......
"""
Page classes to test either the Course Team page or the Library Team page.
"""
from bok_choy.promise import EmptyPromise
from bok_choy.page_object import PageObject
from ...tests.helpers import disable_animations
from . import BASE_URL
def wait_for_ajax_or_reload(browser):
"""
Wait for all ajax requests to finish, OR for the page to reload.
Normal wait_for_ajax() chokes on occasion if the pages reloads,
giving "WebDriverException: Message: u'jQuery is not defined'"
"""
def _is_ajax_finished():
""" Wait for jQuery to finish all AJAX calls, if it is present. """
return browser.execute_script("return typeof(jQuery) == 'undefined' || jQuery.active == 0")
EmptyPromise(_is_ajax_finished, "Finished waiting for ajax requests.").fulfill()
class UsersPage(PageObject):
"""
Base class for either the Course Team page or the Library Team page
"""
def __init__(self, browser, locator):
super(UsersPage, self).__init__(browser)
self.locator = locator
@property
def url(self):
"""
URL to this page - override in subclass
"""
raise NotImplementedError
def is_browser_on_page(self):
"""
Returns True iff the browser has loaded the page.
"""
return self.q(css='body.view-team').present
@property
def users(self):
"""
Return a list of users listed on this page.
"""
return self.q(css='.user-list .user-item').map(lambda el: UserWrapper(self.browser, el.get_attribute('data-email'))).results
@property
def has_add_button(self):
"""
Is the "New Team Member" button present?
"""
return self.q(css='.create-user-button').present
def click_add_button(self):
"""
Click on the "New Team Member" button
"""
self.q(css='.create-user-button').click()
@property
def new_user_form_visible(self):
""" Is the new user form visible? """
return self.q(css='.form-create.create-user .user-email-input').visible
def set_new_user_email(self, email):
""" Set the value of the "New User Email Address" field. """
self.q(css='.form-create.create-user .user-email-input').fill(email)
def click_submit_new_user_form(self):
""" Submit the "New User" form """
self.q(css='.form-create.create-user .action-primary').click()
wait_for_ajax_or_reload(self.browser)
class LibraryUsersPage(UsersPage):
"""
Library Team page in Studio
"""
@property
def url(self):
"""
URL to the "User Access" page for the given library.
"""
return "{}/library/{}/team/".format(BASE_URL, unicode(self.locator))
class UserWrapper(PageObject):
"""
A PageObject representing a wrapper around a user listed on the course/library team page.
"""
url = None
COMPONENT_BUTTONS = {
'basic_tab': '.editor-tabs li.inner_tab_wrap:nth-child(1) > a',
'advanced_tab': '.editor-tabs li.inner_tab_wrap:nth-child(2) > a',
'save_settings': '.action-save',
}
def __init__(self, browser, email):
super(UserWrapper, self).__init__(browser)
self.email = email
self.selector = '.user-list .user-item[data-email="{}"]'.format(self.email)
def is_browser_on_page(self):
"""
Sanity check that our wrapper element is on the page.
"""
return self.q(css=self.selector).present
def _bounded_selector(self, selector):
"""
Return `selector`, but limited to this particular user entry's context
"""
return '{} {}'.format(self.selector, selector)
@property
def name(self):
""" Get this user's username, as displayed. """
return self.q(css=self._bounded_selector('.user-username')).text[0]
@property
def role_label(self):
""" Get this user's role, as displayed. """
return self.q(css=self._bounded_selector('.flag-role .value')).text[0]
@property
def is_current_user(self):
""" Does the UI indicate that this is the current user? """
return self.q(css=self._bounded_selector('.flag-role .msg-you')).present
@property
def can_promote(self):
""" Can this user be promoted to a more powerful role? """
return self.q(css=self._bounded_selector('.add-admin-role')).present
@property
def promote_button_text(self):
""" What does the promote user button say? """
return self.q(css=self._bounded_selector('.add-admin-role')).text[0]
def click_promote(self):
""" Click on the button to promote this user to the more powerful role """
self.q(css=self._bounded_selector('.add-admin-role')).click()
wait_for_ajax_or_reload(self.browser)
@property
def can_demote(self):
""" Can this user be demoted to a less powerful role? """
return self.q(css=self._bounded_selector('.remove-admin-role')).present
@property
def demote_button_text(self):
""" What does the demote user button say? """
return self.q(css=self._bounded_selector('.remove-admin-role')).text[0]
def click_demote(self):
""" Click on the button to demote this user to the less powerful role """
self.q(css=self._bounded_selector('.remove-admin-role')).click()
wait_for_ajax_or_reload(self.browser)
@property
def can_delete(self):
""" Can this user be deleted? """
return self.q(css=self._bounded_selector('.action-delete:not(.is-disabled) .remove-user')).present
def click_delete(self):
""" Click the button to delete this user. """
disable_animations(self)
self.q(css=self._bounded_selector('.remove-user')).click()
# We can't use confirm_prompt because its wait_for_ajax is flaky when the page is expected to reload.
self.wait_for_element_visibility('.prompt', 'Prompt is visible')
self.wait_for_element_visibility('.prompt .action-primary', 'Confirmation button is visible')
self.q(css='.prompt .action-primary').click()
wait_for_ajax_or_reload(self.browser)
@property
def has_no_change_warning(self):
""" Does this have a warning in place of the promote/demote buttons? """
return self.q(css=self._bounded_selector('.notoggleforyou')).present
@property
def no_change_warning_text(self):
""" Text of the warning seen in place of the promote/demote buttons. """
return self.q(css=self._bounded_selector('.notoggleforyou')).text[0]
...@@ -5,8 +5,10 @@ from ddt import ddt, data ...@@ -5,8 +5,10 @@ from ddt import ddt, data
from .base_studio_test import StudioLibraryTest from .base_studio_test import StudioLibraryTest
from ...fixtures.course import XBlockFixtureDesc from ...fixtures.course import XBlockFixtureDesc
from ...pages.studio.auto_auth import AutoAuthPage
from ...pages.studio.utils import add_component from ...pages.studio.utils import add_component
from ...pages.studio.library import LibraryPage from ...pages.studio.library import LibraryPage
from ...pages.studio.users import LibraryUsersPage
@ddt @ddt
...@@ -306,3 +308,138 @@ class LibraryNavigationTest(StudioLibraryTest): ...@@ -306,3 +308,138 @@ class LibraryNavigationTest(StudioLibraryTest):
self.assertEqual(self.lib_page.xblocks[0].name, '1') self.assertEqual(self.lib_page.xblocks[0].name, '1')
self.assertEqual(self.lib_page.xblocks[-1].name, '11') self.assertEqual(self.lib_page.xblocks[-1].name, '11')
self.assertEqual(self.lib_page.get_page_number(), '1') self.assertEqual(self.lib_page.get_page_number(), '1')
class LibraryUsersPageTest(StudioLibraryTest):
"""
Test the functionality of the library "Instructor Access" page.
"""
def setUp(self):
super(LibraryUsersPageTest, self).setUp()
# Create a second user for use in these tests:
AutoAuthPage(self.browser, username="second", email="second@example.com", no_login=True).visit()
self.page = LibraryUsersPage(self.browser, self.library_key)
self.page.visit()
def _expect_refresh(self):
"""
Wait for the page to reload.
"""
self.page = LibraryUsersPage(self.browser, self.library_key).wait_for_page()
def test_user_management(self):
"""
Scenario: Ensure that we can edit the permissions of users.
Given I have a library in Studio where I am the only admin
assigned (which is the default for a newly-created library)
And I navigate to Library "Instructor Access" Page in Studio
Then there should be one user listed (myself), and I must
not be able to remove myself or my instructor privilege.
When I click Add Intructor
Then I see a form to complete
When I complete the form and submit it
Then I can see the new user is listed as a "User" of the library
When I click to Add Staff permissions to the new user
Then I can see the new user has staff permissions and that I am now
able to promote them to an Admin or remove their staff permissions.
When I click to Add Admin permissions to the new user
Then I can see the new user has admin permissions and that I can now
remove Admin permissions from either user.
"""
def check_is_only_admin(user):
"""
Ensure user is an admin user and cannot be removed.
(There must always be at least one admin user.)
"""
self.assertIn("admin", user.role_label.lower())
self.assertFalse(user.can_promote)
self.assertFalse(user.can_demote)
self.assertFalse(user.can_delete)
self.assertTrue(user.has_no_change_warning)
self.assertIn("Promote another member to Admin to remove admin rights", user.no_change_warning_text)
self.assertEqual(len(self.page.users), 1)
user = self.page.users[0]
self.assertTrue(user.is_current_user)
check_is_only_admin(user)
# Add a new user:
self.assertTrue(self.page.has_add_button)
self.assertFalse(self.page.new_user_form_visible)
self.page.click_add_button()
self.assertTrue(self.page.new_user_form_visible)
self.page.set_new_user_email('second@example.com')
self.page.click_submit_new_user_form()
# Check the new user's listing:
def get_two_users():
"""
Expect two users to be listed, one being me, and another user.
Returns me, them
"""
users = self.page.users
self.assertEqual(len(users), 2)
self.assertEqual(len([u for u in users if u.is_current_user]), 1)
if users[0].is_current_user:
return users[0], users[1]
else:
return users[1], users[0]
self._expect_refresh()
user_me, them = get_two_users()
check_is_only_admin(user_me)
self.assertIn("user", them.role_label.lower())
self.assertTrue(them.can_promote)
self.assertIn("Add Staff Access", them.promote_button_text)
self.assertFalse(them.can_demote)
self.assertTrue(them.can_delete)
self.assertFalse(them.has_no_change_warning)
# Add Staff permissions to the new user:
them.click_promote()
self._expect_refresh()
user_me, them = get_two_users()
check_is_only_admin(user_me)
self.assertIn("staff", them.role_label.lower())
self.assertTrue(them.can_promote)
self.assertIn("Add Admin Access", them.promote_button_text)
self.assertTrue(them.can_demote)
self.assertIn("Remove Staff Access", them.demote_button_text)
self.assertTrue(them.can_delete)
self.assertFalse(them.has_no_change_warning)
# Add Admin permissions to the new user:
them.click_promote()
self._expect_refresh()
user_me, them = get_two_users()
self.assertIn("admin", user_me.role_label.lower())
self.assertFalse(user_me.can_promote)
self.assertTrue(user_me.can_demote)
self.assertTrue(user_me.can_delete)
self.assertFalse(user_me.has_no_change_warning)
self.assertIn("admin", them.role_label.lower())
self.assertFalse(them.can_promote)
self.assertTrue(them.can_demote)
self.assertIn("Remove Admin Access", them.demote_button_text)
self.assertTrue(them.can_delete)
self.assertFalse(them.has_no_change_warning)
# Delete the new user:
them.click_delete()
self._expect_refresh()
self.assertEqual(len(self.page.users), 1)
user = self.page.users[0]
self.assertTrue(user.is_current_user)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment