pages.py 30.1 KB
Newer Older
1 2 3
"""
Page objects for UI-level acceptance tests.
"""
4 5 6

import os

7
from bok_choy.page_object import PageObject
8
from bok_choy.promise import BrokenPromise, EmptyPromise
9

10
ORA_SANDBOX_URL = os.environ.get('ORA_SANDBOX_URL')
11 12


13 14 15 16 17
class PageConfigurationError(Exception):
    """ A page object was not configured correctly. """
    pass


18
class BaseAssessmentPage(PageObject):
19 20 21 22 23 24 25 26 27 28 29 30
    """
    Base class for ORA page objects.
    """
    def __init__(self, browser, problem_location):
        """
        Configure a page object for a particular ORA problem.

        Args:
            browser (Selenium browser): The browser object used by the tests.
            problem_location (unicode): URL path for the problem, appended to the base URL.

        """
31
        super(BaseAssessmentPage, self).__init__(browser)
32 33
        self._problem_location = problem_location

34 35 36 37 38 39 40
    @property
    def url(self):
        return "{base}/{loc}".format(
            base=ORA_SANDBOX_URL,
            loc=self._problem_location
        )

41 42 43
    def get_sr_html(self):
        return self.q(css='.sr.reader-feedback').html

44 45 46 47 48 49
    def confirm_feedback_text(self, text):
        def is_text_in_feedback():
            return text in self.get_sr_html()[0]

        self.wait_for(is_text_in_feedback, 'Waiting for %s, in %s' % (text, self.q(css='.sr.reader-feedback').html[0]))

50

51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
class MultipleAssessmentPage(BaseAssessmentPage):
    """
    Page object for subsection unit containing multiple ORA problems
    Does not inherit OpenAssessmentPage object
    Used to test accessibility of multiple ORA problems on one page
    """
    def is_browser_on_page(self):
        # Css is #main to scope the page object to the entire main problem area of a unit
        # For testing multiple ORA problems on one page, we don't want to scope it to a particular Xblock
        return self.q(css='#main').is_present()


class OpenAssessmentPage(BaseAssessmentPage):
    """
    Base class for singular ORA page objects.
    """
    # vertical index is the index identifier of a problem component on a page.
    vertical_index = 0

70 71 72 73 74 75
    def _bounded_selector(self, selector):
        """
        Allows scoping to a portion of the page.

        The default implementation just returns the selector
        """
76 77 78
        return "{vertical_index_class} {selector}".format(
            vertical_index_class=self.vertical_index_class, selector=selector
        )
79 80 81 82 83 84 85 86

    @property
    def vertical_index_class(self):
        """
        Every problem has a vertical index assigned to it, which creates a vertical index class like
        `vert-{vertical_index}. If there is one problem on unit page, problem would have .vert-0 class attached to it.
        """
        return ".vert-{vertical_index}".format(vertical_index=self.vertical_index)
87 88

    def submit(self, button_css=".action--submit"):
89 90 91
        """
        Click the submit button on the page.
        This relies on the fact that we use the same CSS styles for submit buttons
92
        in all problem steps (unless custom value for button_css is passed in).
93
        """
94
        submit_button_selector = self._bounded_selector(button_css)
95
        EmptyPromise(
Eric Fischer committed
96
            lambda: not any(self.q(css=submit_button_selector).attrs('disabled')),
97 98 99 100
            "Submit button is enabled."
        ).fulfill()

        with self.handle_alert():
101
            self.q(css=submit_button_selector).first.click()
102

Pan Luo committed
103 104 105 106
    def hide_django_debug_tool(self):
        if self.q(css='#djDebug').visible:
            self.q(css='#djHideToolBarButton').click()

107 108 109
    def button(self, button_css):
        return self.q(css=button_css + " > .ui-slidable")

110 111 112 113 114 115 116

class SubmissionPage(OpenAssessmentPage):
    """
    Page object representing the "submission" step in an ORA problem.
    """

    def is_browser_on_page(self):
117
        return self.q(css='.step--response').is_present()
118 119 120 121 122 123 124 125 126 127 128 129

    def submit_response(self, response_text):
        """
        Submit a response for the problem.

        Args:
            response_text (unicode): The submission response text.

        Raises:
            BrokenPromise: The response was not submitted successfully.

        """
130 131 132
        textarea_element = self._bounded_selector("textarea.submission__answer__part__text__value")
        self.wait_for_element_visibility(textarea_element, "Textarea is present")
        self.q(css=textarea_element).fill(response_text)
133 134 135
        self.submit()
        EmptyPromise(lambda: self.has_submitted, 'Response is completed').fulfill()

136 137 138 139 140 141
    def fill_latex(self, latex_query):
        """
        Fill the latex expression
        Args:
         latex_query (unicode): Latex expression text
        """
142 143
        textarea_element = self._bounded_selector("textarea.submission__answer__part__text__value")
        self.wait_for_element_visibility(textarea_element, "Textarea is present")
144 145 146
        self.q(css="textarea.submission__answer__part__text__value").fill(latex_query)

    def preview_latex(self):
147
        # Click 'Preview in LaTeX' button on the page.
148 149
        self.q(css="button.submission__preview").click()
        self.wait_for_element_visibility(".preview_content .MathJax_SVG", "Verify Preview LaTeX expression")
150

Pan Luo committed
151 152 153 154 155 156 157
    def select_file(self, file_path_name):
        """
        Select a file from local file system for uploading

        Args:
          file_path_name (string): full path and name of the file
        """
158 159
        self.wait_for_element_visibility(".submission__answer__upload", "File select button is present")
        self.q(css=".submission__answer__upload").results[0].send_keys(file_path_name)
Pan Luo committed
160

161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180
    def add_file_description(self, file_num, description):
        """
        Submit a description for some file.

        Args:
          file_num (integer): file number
          description (string): file description
        """
        textarea_element = self._bounded_selector("textarea.file__description__%d" % file_num)
        self.wait_for_element_visibility(textarea_element, "Textarea is present")
        self.q(css=textarea_element).fill(description)

    @property
    def upload_file_button_is_enabled(self):
        """
        Check if 'Upload files' button is enabled

        Returns:
            bool
        """
Eric Fischer committed
181
        return self.q(css="button.file__upload")[0].is_enabled()
182 183 184 185 186 187 188 189 190 191 192

    @property
    def upload_file_button_is_disabled(self):
        """
        Check if 'Upload files' button is disabled

        Returns:
            bool
        """
        return self.q(css="button.file__upload").attrs('disabled') == ['true']

Pan Luo committed
193 194 195 196
    def upload_file(self):
        """
        Upload the selected file
        """
197 198
        self.wait_for_element_visibility(".file__upload", "Upload button is present")
        self.q(css=".file__upload").click()
Pan Luo committed
199

200 201 202 203 204 205 206 207
    @property
    def latex_preview_button_is_disabled(self):
        """
        Check if 'Preview in Latex' button is disabled

        Returns:
            bool
        """
208
        return self.q(css="button.submission__preview").attrs('disabled') == ['true']
209

210 211 212 213 214 215 216 217 218 219
    @property
    def has_submitted(self):
        """
        Check whether the response was submitted successfully.

        Returns:
            bool
        """
        return self.q(css=".step--response.is--complete").is_present()

Pan Luo committed
220 221 222 223 224 225 226 227
    @property
    def has_file_error(self):
        """
        Check whether there is an error message for file upload.

        Returns:
            bool
        """
228
        return self.q(css="div.upload__error > div.message--error").visible
Pan Luo committed
229 230

    @property
231
    def have_files_uploaded(self):
Pan Luo committed
232
        """
233
        Check whether files were successfully uploaded
Pan Luo committed
234 235 236 237

        Returns:
            bool
        """
238 239
        self.wait_for_element_visibility('.submission__custom__upload', 'Uploaded files block is presented')
        return self.q(css=".submission__answer__files").visible
Pan Luo committed
240

241

242 243 244 245
class AssessmentMixin(object):
    """
    Mixin for interacting with the assessment rubric.
    """
246
    def assess(self, options_selected):
247 248 249 250 251 252 253 254 255 256 257 258 259 260
        """
        Create an assessment.

        Args:
            options_selected (list of int): list of the indices (starting from 0)
            of each option to select in the rubric.

        Returns:
            AssessmentPage

        Example usage:
        >>> page.assess([0, 2, 1])

        """
261
        def selector(criterion_num, option_num):
262
            sel = ".rubric_{criterion_num}_{option_num}".format(
263 264 265
                criterion_num=criterion_num,
                option_num=option_num
            )
266 267 268 269 270 271 272 273 274 275
            return sel

        def select_criterion():
            for criterion_num, option_num in enumerate(options_selected):
                sel = selector(criterion_num, option_num)
                self.q(css=self._bounded_selector(sel)).first.click()

        def criterion_selected():
            for criterion_num, option_num in enumerate(options_selected):
                sel = selector(criterion_num, option_num)
276
                self.wait_for_element_visibility(self._bounded_selector(sel), "Criterion option visible")
277 278 279 280 281 282 283 284 285 286 287 288
                if not self.q(css=self._bounded_selector(sel))[0].is_selected():
                    return False
            return True

        # When selecting the criteria for the 2nd training assessment, sometimes
        # radio buttons are not selected after the click, causing the test to fail (because the
        # Submit button never becomes enabled). Since tests that use training assessments tend
        # to be very long (meaning there is a high cost to retrying the whole test),
        # retry just selecting the criterion a few times before failing the whole test.
        attempts = 0
        while not criterion_selected() and attempts < 5:
            select_criterion()
Eric Fischer committed
289
            attempts += 1
290

291 292 293
        self.submit_assessment()
        return self

Eric Fischer committed
294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311
    def provide_criterion_feedback(self, feedback):
        """
        Provides feedback for the first criterion on a given assessment, without submitting the assessment.

        Args:
            feedback (string): the feedback to be recorded
        """
        self.q(css=self._bounded_selector('.answer--feedback .answer__value')).first.fill(feedback)

    def provide_overall_feedback(self, feedback):
        """
        Provides overall feedback for a given assessment, without submitting the assessment.

        Args:
            feedback (string): the feedback to be recorded
        """
        self.q(css=self._bounded_selector('.assessment__rubric__question--feedback__value')).first.fill(feedback)

312 313 314 315 316 317 318 319
    def submit_assessment(self):
        """
        Submit an assessment of the problem.
        """
        self.submit()


class AssessmentPage(OpenAssessmentPage, AssessmentMixin):
320
    """
321
    Page object representing an "assessment" step in an ORA problem.
322 323
    """

324
    ASSESSMENT_TYPES = ['self-assessment', 'peer-assessment', 'student-training', 'staff-assessment']
325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342

    def __init__(self, assessment_type, *args):
        """
        Configure which assessment type this page object represents.

        Args:
            assessment_type: One of the valid assessment types.
            *args: Passed to the base class.

        """
        super(AssessmentPage, self).__init__(*args)
        if assessment_type not in self.ASSESSMENT_TYPES:
            msg = "Invalid assessment type; must choose one: {choices}".format(
                choices=", ".join(self.ASSESSMENT_TYPES)
            )
            raise PageConfigurationError(msg)
        self._assessment_type = assessment_type

343 344 345 346
    def _bounded_selector(self, selector):
        """
        Return `selector`, but limited to this Assignment Page.
        """
347
        return super(AssessmentPage, self)._bounded_selector('.step--{assessment_type} {selector}'.format(
348
            assessment_type=self._assessment_type, selector=selector))
349

350
    def is_browser_on_page(self):
351
        css_class = ".step--{assessment_type}".format(
352 353
            assessment_type=self._assessment_type
        )
354
        return self.q(css=css_class).is_present()
355

356
    @property
357 358 359 360 361 362 363
    def response_text(self):
        """
        Retrieve the text of the response shown in the assessment.

        Returns:
            unicode
        """
364
        css_sel = ".{assessment_type}__display .submission__answer__part__text__value".format(
365 366
            assessment_type=self._assessment_type
        )
367
        return u" ".join(self.q(css=self._bounded_selector(css_sel)).text)
368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422

    def wait_for_complete(self):
        """
        Wait until the assessment step is marked as complete.

        Raises:
            BrokenPromise

        returns:
            AssessmentPage

        """
        EmptyPromise(lambda: self.is_complete, 'Assessment is complete').fulfill()
        return self

    def wait_for_response(self):
        """
        Wait for response text to be available.

        Raises:
            BrokenPromise

        Returns:
            AssessmentPage
        """
        EmptyPromise(
            lambda: len(self.response_text) > 0,
            "Has response text."
        ).fulfill()
        return self

    def wait_for_num_completed(self, num_completed):
        """
        Wait for at least a certain number of assessments
        to be completed.

        Can only be used with peer-assessment and student-training.

        Args:
            num_completed (int): The number of assessments we expect
                to be completed.

        Raises:
            PageConfigurationError
            BrokenPromise

        Returns:
            AssessmentPage

        """
        EmptyPromise(
            lambda: self.num_completed >= num_completed,
            "Completed at least one assessment."
        ).fulfill()
        return self
423 424

    @property
425
    def is_complete(self):
426 427 428 429 430 431
        """
        Check whether the assessment was submitted successfully.

        Returns:
            bool
        """
432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452
        css_sel = ".step--{assessment_type}.is--complete".format(
            assessment_type=self._assessment_type
        )
        return self.q(css=css_sel).is_present()

    @property
    def num_completed(self):
        """
        Retrieve the number of completed assessments.
        Can only be used for peer-assessment and student-training.

        Returns:
            int

        Raises:
            PageConfigurationError

        """
        if self._assessment_type not in ['peer-assessment', 'student-training']:
            msg = "Only peer assessment and student training steps can retrieve the number completed"
            raise PageConfigurationError(msg)
453

454
        status_completed_css = self._bounded_selector(".step__status__value--completed")
455 456 457 458 459 460 461 462 463 464
        complete_candidates = [int(x) for x in self.q(css=status_completed_css).text]
        if len(complete_candidates) > 0:
            completed = complete_candidates[0]
        else:
            # The number completed is no longer available on this page, but can be inferred from the
            # current review number.
            status_current_css = self._bounded_selector(".step__status__number--current")
            current_candidates = [int(y) for y in self.q(css=status_current_css).text]
            completed = current_candidates[0] - 1 if len(current_candidates) > 0 and current_candidates[0] > 0 else None
        return completed
465

466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496
    @property
    def label(self):
        """
        Returns the label of this assessment step.

        Returns:
            string
        """
        return self.q(css=self._bounded_selector(".step__label")).text[0]

    @property
    def status_value(self):
        """
        Returns the status value (ie., "COMPLETE", "CANCELLED", etc.) of this assessment step.

        Returns:
            string
        """
        return self.q(css=self._bounded_selector(".step__status__value")).text[0]

    @property
    def message_title(self):
        """
        Returns the message title, if present, of this assesment step.

        Returns:
            string is message title is present, else None
        """
        message_title = self.q(css=self._bounded_selector(".message__title"))
        if len(message_title) == 0:
            return None
497

498 499 500 501 502 503 504 505 506 507 508
        return message_title.text[0]

    def verify_status_value(self, expected_value):
        """
        Waits until the expected status value appears. If it does not appear, fails the test.
        """
        EmptyPromise(
            lambda: self.status_value == expected_value,
            "Expected status value present"
        ).fulfill()

509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527
    def open_step(self):
        """
        Opens the current step if it is not already open

        Returns:
            AssessmentPage
        """
        container = self._bounded_selector("")

        if 'is--showing' not in " ".join(self.q(css=container).attrs('class')):
            self.q(css=self._bounded_selector(".ui-slidable")).click()

        EmptyPromise(
            lambda: 'is--showing' in " ".join(self.q(css=container).attrs('class')),
            "Step is showing"
        )

        return self

528 529 530 531 532

class GradePage(OpenAssessmentPage):
    """
    Page object representing the "grade" step in an ORA problem.
    """
533 534 535 536
    def _bounded_selector(self, selector):
        """
        Return `selector`, but limited to the student grade view.
        """
537
        return super(GradePage, self)._bounded_selector('.step--grade {selector}'.format(selector=selector))
538 539

    def is_browser_on_page(self):
540
        return self.q(css=".step--grade").is_present()
541 542 543 544 545 546 547 548 549 550 551 552

    @property
    def score(self):
        """
        Retrieve the number of points received.

        Returns:
            int or None

        Raises:
            ValueError if the score is not an integer.
        """
Eric Fischer committed
553 554 555 556 557 558 559 560
        earned_selector = self._bounded_selector(".grade__value__earned")
        score_candidates = []
        try:
            self.wait_for_element_visibility(earned_selector, "Earned score was visible", 2)
            score_candidates = [int(x) for x in self.q(css=earned_selector).text]
        except BrokenPromise:
            # Sometimes there is no score, and that's expected.
            pass
561
        return score_candidates[0] if len(score_candidates) > 0 else None
562

563
    def grade_entry(self, question):
564
        """
565
        Returns a tuple of the text of all answer spans for a given question
566 567 568 569

        Args:
            question: the 0-based question for which to get grade information.

570
        Returns: a tuple containing all text elements.
571 572

        """
573
        self.wait_for_element_visibility(
574 575
            self._bounded_selector('.question--{} .answer'.format(question + 1)),
            "Answers not present",
576 577
            2
        )
578

579 580 581 582
        selector_str = ".question--{} .answer div span".format(question + 1)
        span_text = self.q(
            css=self._bounded_selector(selector_str)
        )
583

584 585
        result = tuple(span_entry.text.strip() for span_entry in span_text if span_entry.text != '')
        return result
586

Eric Fischer committed
587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628
    def feedback_entry(self, question, column):
        """
        Returns the recorded feedback for a specific grade source.

        Args:
            question: the 0-based question for which to get grade information. Note that overall feedback can
                be acquired by using 'feedback' for this parameter
            column: the 0-based column of data within a question. Each column corresponds
                to a source of data (for example, staff, peer, or self).

        Returns: the recorded feedback for the requested grade source.

        """
        if isinstance(question, int):
            question = question + 1
        self.wait_for_element_visibility(
            self._bounded_selector('.question--{} .feedback__value'.format(question)),
            "Feedback is visible",
        )
        feedback = self.q(
            css=self._bounded_selector('.question--{} .feedback__value'.format(question))
        )

        return feedback[column].text.strip()

    @property
    def total_reported_answers(self):
        """
        Returns the total number of reported answers. A "reported answer" is any option or feedback item for a
        (criterion, assessment_type) pair. For example, if there are 2 criterion, each with options and feedback,
        and 2 assessment types, the total number of reported answers will be 8 (2 for each of option/feedback, for
        2 questions, for 2 assessment types = 2*2*2 = 8)
        """
        return len(self.q(css=self._bounded_selector('.answer')))

    @property
    def number_scored_criteria(self):
        """
        Returns the number of criteria with a score on the grade page.
        """
        return len(self.q(css=self._bounded_selector('.question__score')))

629

630
class StaffAreaPage(OpenAssessmentPage, AssessmentMixin):
631
    """
632
    Page object representing the tabbed staff area.
633 634
    """

635 636 637 638
    def _bounded_selector(self, selector):
        """
        Return `selector`, but limited to the staff area management area.
        """
639
        return super(StaffAreaPage, self)._bounded_selector('.openassessment__staff-area {}'.format(selector))
640

641
    def is_browser_on_page(self):
642
        return self.q(css=".openassessment__staff-area").is_present()
643 644 645 646 647 648

    @property
    def selected_button_names(self):
        """
        Returns the names of the selected toolbar buttons.
        """
649
        buttons = self.q(css=self._bounded_selector(".ui-staff__button"))
650 651 652 653 654
        return [button.text for button in buttons if u'is--active' in button.get_attribute('class')]

    @property
    def visible_staff_panels(self):
        """
655
        Returns the classes of the visible staff panels
656
        """
657
        panels = self.q(css=self._bounded_selector(".wrapper--ui-staff"))
658
        return [panel.get_attribute('class') for panel in panels if panel.is_displayed()]
659

660 661 662 663 664 665 666
    def is_button_visible(self, button_name):
        """
        Returns True if button_name is visible, else False
        """
        button = self.q(css=self._bounded_selector(".button-{button_name}".format(button_name=button_name)))
        return button.is_present()

667 668 669 670 671
    def click_staff_toolbar_button(self, button_name):
        """
        Presses the button to show the panel with the specified name.
        :return:
        """
672 673
        # Disable JQuery animations (for slideUp/slideDown).
        self.browser.execute_script("jQuery.fx.off = true;")
Eric Fischer committed
674 675 676
        button_selector = self._bounded_selector(".button-{button_name}".format(button_name=button_name))
        self.wait_for_element_visibility(button_selector, "Button {} is present".format(button_name))
        buttons = self.q(css=button_selector)
677 678 679 680 681 682 683
        buttons.first.click()

    def click_staff_panel_close_button(self, panel_name):
        """
        Presses the close button on the staff panel with the specified name.
        :return:
        """
684 685 686
        self.q(
            css=self._bounded_selector(".wrapper--{panel_name} .ui-staff_close_button".format(panel_name=panel_name))
        ).click()
687 688 689 690 691 692

    def show_learner(self, username):
        """
        Clicks the staff tools panel and and searches for learner information about the given username.
        """
        self.click_staff_toolbar_button("staff-tools")
693 694 695 696
        student_input_css = self._bounded_selector("input.openassessment__student_username")
        self.wait_for_element_visibility(student_input_css, "Input is present")
        self.q(css=student_input_css).fill(username)
        submit_button = self.q(css=self._bounded_selector(".action--submit-username"))
697
        submit_button.first.click()
Eric Fischer committed
698 699 700 701
        self.wait_for_element_visibility(
            self._bounded_selector(".staff-info__student__report"),
            "Student report is present"
        )
702

703 704 705 706 707
    def expand_staff_grading_section(self):
        """
        Clicks the staff grade control to expand staff grading section for use in staff required workflows.
        """
        self.q(css=self._bounded_selector(".staff__grade__show-form")).first.click()
Eric Fischer committed
708 709 710 711
        self.wait_for_element_visibility(
            ".staff-full-grade__assessment__rubric__question--0",
            "staff grading is present"
        )
712 713 714 715 716 717 718 719 720 721 722 723 724 725 726

    @property
    def available_checked_out_numbers(self):
        """
        Gets "N available and M checked out" information from staff grading sections.
        Returns tuple of (N, M)
        """
        raw_string = self.q(css=self._bounded_selector(".staff__grade__value")).text[0]
        ret = tuple(int(s) for s in raw_string.split() if s.isdigit())
        if len(ret) != 2:
            raise PageConfigurationError("Unable to parse available and checked out numbers")
        return ret

    def verify_available_checked_out_numbers(self, expected_value):
        """
Eric Fischer committed
727 728
        Waits until the expected value for available and checked out numbers appears. If it does not appear, fails the
        test.
729 730 731 732 733 734 735 736

        expected_value should be a tuple as described in the available_checked_out_numbers property above.
        """
        EmptyPromise(
            lambda: self.available_checked_out_numbers == expected_value,
            "Expected avaiable and checked out values present"
        ).fulfill()

737 738 739 740 741
    @property
    def learner_report_text(self):
        """
        Returns the text present in the learner report (useful for case where there is no response).
        """
742 743 744 745 746 747 748 749 750 751
        return self.q(css=self._bounded_selector(".staff-info__student__report")).text[0]

    def verify_learner_report_text(self, expectedText):
        """
        Verifies the learner report text is as expected.
        """
        EmptyPromise(
            lambda: self.learner_report_text == expectedText,
            "Learner report text correct"
        ).fulfill()
752 753 754 755 756 757

    @property
    def learner_report_sections(self):
        """
        Returns the titles of the collapsible learner report sections present on the page.
        """
758 759
        self.wait_for_section_titles()
        sections = self.q(css=self._bounded_selector(".ui-staff__subtitle"))
760
        return [section.text for section in sections]
761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782

    def wait_for_section_titles(self):
        """
        Wait for section titles to appear.
        """
        EmptyPromise(
            lambda: len(self.q(css=self._bounded_selector(".ui-staff__subtitle"))) > 0,
            "Section titles appeared"
        ).fulfill()

    def expand_learner_report_sections(self):
        """
        Expands all the sections in the learner report.
        """
        self.wait_for_section_titles()
        self.q(css=self._bounded_selector(".ui-staff__subtitle")).click()

    @property
    def learner_final_score(self):
        """
        Returns the final score displayed in the learner report.
        """
783
        score = self.q(css=self._bounded_selector(".staff-info__final__grade__score"))
784 785 786 787 788 789 790 791 792 793 794 795 796 797
        if len(score) == 0:
            return None
        return score.text[0]

    def verify_learner_final_score(self, expected_score):
        """
        Verifies that the final score in the learner report is equal to the expected value.
        """
        EmptyPromise(
            lambda: self.learner_final_score == expected_score,
            "Learner score is updated"
        ).fulfill()

    @property
798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818
    def learner_final_score_table_headers(self):
        """
        Return the final score table headers (as an array of strings) as shown in the staff area section.

        Returns: array of strings representing the headers (for example,
            ['CRITERION', 'STAFF GRADE', 'PEER MEDIAN GRADE', 'SELF ASSESSMENT GRADE'])
        """
        return self._get_table_text(".staff-info__final__grade__table th")

    @property
    def learner_final_score_table_values(self):
        """
        Return the final score table values (as an array of strings) as shown in the staff area section.

        Returns: array of strings representing the text (for example,
            ['Poor - 0 points', 'Waiting for peer reviews', 'Good',
             'Fair - 1 point', 'Waiting for peer reviews', 'Excellent'])
        """
        return self._get_table_text(".staff-info__final__grade__table .value")

    @property
819 820
    def learner_response(self):
        return self.q(
821
            css=self._bounded_selector(".staff-info__student__response .ui-slidable__content")
822 823
        ).text[0]

824
    def staff_assess(self, options_selected, continue_after=False):
825
        for criterion_num, option_num in enumerate(options_selected):
826
            sel = ".rubric_{criterion_num}_{option_num}".format(
827 828 829 830 831 832 833
                criterion_num=criterion_num,
                option_num=option_num
            )
            self.q(css=self._bounded_selector(sel)).first.click()
        self.submit_assessment(continue_after)

    def submit_assessment(self, continue_after=False):
834 835 836
        """
        Submit a staff assessment of the problem.
        """
837 838 839 840
        filter_text = "Submit assessment"
        if continue_after:
            filter_text += " and continue grading"
        self.q(css=self._bounded_selector("button.action--submit")).filter(text=filter_text).first.click()
841 842 843 844 845 846 847 848

    def cancel_submission(self):
        """
        Cancel a learner's assessment.
        """
        # Must put a comment to enable the submit button.
        self.q(css=self._bounded_selector("textarea.cancel_submission_comments")).fill("comment")
        self.submit(button_css=".action--submit-cancel-submission")
cahrens committed
849 850 851 852 853 854 855

    def status_text(self, section):
        """
        Return the status text (as an array of strings) as shown in the staff area section.

        Args:
            section: the classname of the section for which text should be returned
Eric Fischer committed
856
                (for example, 'peer__assessments', 'submitted__assessments', or 'self__assessments'
cahrens committed
857 858 859 860

        Returns: array of strings representing the text(for example, ['Good', u'5', u'5', u'Excellent', u'3', u'3'])

        """
861
        return self._get_table_text(".staff-info__{} .staff-info__status__table .value".format(section))
cahrens committed
862

Eric Fischer committed
863 864 865 866 867 868 869 870 871 872
    def overall_feedback(self, section):
        """
        Return the overall feedback (a string otherwise excluded from status_text) as shown in the staff area section.

        Args:
            section: the classname of the section for which text should be returned
                (for example, 'peer__assessments', 'submitted__assessments', or 'self__assessments'

        Returns: the text present in "Overall Feedback"
        """
Eric Fischer committed
873 874 875
        return self.q(
            css=self._bounded_selector(".staff-info__{} .student__answer__display__content".format(section))
        ).text[0]
Eric Fischer committed
876

877 878 879 880
    def _get_table_text(self, selector):
        """
        Helper method for getting text out of a table.
        """
cahrens committed
881
        table_elements = self.q(
882
            css=self._bounded_selector(selector)
cahrens committed
883 884
        )
        text = []
885 886
        for element in table_elements:
            text.append(element.text)
cahrens committed
887
        return text