From 95af18430cea8e7c5fa1e97a56c1a278bd28f985 Mon Sep 17 00:00:00 2001 From: Andy Armstrong Date: Wed, 23 Nov 2016 23:03:39 -0500 Subject: [PATCH] Fix inline discussion Bok Choy tests --- common/test/acceptance/fixtures/discussion.py | 1 + .../test/acceptance/pages/lms/discussion.py | 135 +++++++++--------- .../acceptance/tests/discussion/helpers.py | 16 +++ .../tests/discussion/test_cohorts.py | 4 +- .../tests/discussion/test_discussion.py | 106 +++++++------- .../test/acceptance/tests/lms/test_teams.py | 18 +-- 6 files changed, 149 insertions(+), 131 deletions(-) diff --git a/common/test/acceptance/fixtures/discussion.py b/common/test/acceptance/fixtures/discussion.py index 0607625fb0..4b1b0e70e5 100644 --- a/common/test/acceptance/fixtures/discussion.py +++ b/common/test/acceptance/fixtures/discussion.py @@ -51,6 +51,7 @@ class Thread(ContentFactory): group_id = None pinned = False read = False + context = "course" class Comment(ContentFactory): diff --git a/common/test/acceptance/pages/lms/discussion.py b/common/test/acceptance/pages/lms/discussion.py index 13223e6124..dd470c1cbf 100644 --- a/common/test/acceptance/pages/lms/discussion.py +++ b/common/test/acceptance/pages/lms/discussion.py @@ -15,6 +15,54 @@ class DiscussionPageMixin(object): def is_ajax_finished(self): return self.browser.execute_script("return jQuery.active") == 0 + def find_visible_element(self, selector): + """ + Finds a single visible element with the specified selector. + """ + full_selector = selector + if self.root_selector: + full_selector = self.root_selector + " " + full_selector + elements = self.q(css=full_selector) + return next((element for element in elements if element.is_displayed()), None) + + @property + def new_post_button(self): + """ + Returns the new post button if visible, else it returns None. + """ + return self.find_visible_element(".new-post-btn") + + @property + def new_post_form(self): + """ + Returns the new post form if visible, else it returns None. + """ + return self.find_visible_element(".forum-new-post-form") + + def click_new_post_button(self): + """ + Clicks the 'New Post' button. + """ + self.wait_for( + lambda: self.new_post_button, + description="Waiting for new post button" + ) + self.new_post_button.click() + self.wait_for( + lambda: self.new_post_form, + description="Waiting for new post form" + ) + + def click_cancel_new_post(self): + """ + Clicks the 'Cancel' button from the new post form. + """ + self.click_element(".cancel") + self.wait_for( + lambda: not self.new_post_form, + "Waiting for new post form to close" + ) + class DiscussionThreadPage(PageObject, DiscussionPageMixin): url = None @@ -470,12 +518,15 @@ class DiscussionTabSingleThreadPage(CoursePage): return len(self.q(css=".forum-nav-thread").results) == thread_count -class InlineDiscussionPage(PageObject): +class InlineDiscussionPage(PageObject, DiscussionPageMixin): + """ + Acceptance tests for inline discussions. + """ url = None def __init__(self, browser, discussion_id): super(InlineDiscussionPage, self).__init__(browser) - self._discussion_selector = ( + self.root_selector = ( ".discussion-module[data-discussion-id='{discussion_id}'] ".format( discussion_id=discussion_id ) @@ -486,11 +537,11 @@ class InlineDiscussionPage(PageObject): Returns a query corresponding to the given CSS selector within the scope of this discussion page """ - return self.q(css=self._discussion_selector + " " + selector) + return self.q(css=self.root_selector + " " + selector) def is_browser_on_page(self): self.wait_for_ajax() - return self.q(css=self._discussion_selector).present + return self.q(css=self.root_selector).present def is_discussion_expanded(self): return self._find_within(".discussion").present @@ -506,58 +557,41 @@ class InlineDiscussionPage(PageObject): def get_num_displayed_threads(self): return len(self._find_within(".forum-nav-thread")) - def has_thread(self, thread_id): - """Returns true if this page is showing the thread with the specified id.""" - return self._find_within('.discussion-thread#thread_{}'.format(thread_id)).present - def element_exists(self, selector): - return self.q(css=self._discussion_selector + " " + selector).present - - def is_new_post_opened(self): - return self._find_within(".new-post-article").visible + return self.q(css=self.root_selector + " " + selector).present def click_element(self, selector): self.wait_for_element_presence( - "{discussion} {selector}".format(discussion=self._discussion_selector, selector=selector), + "{discussion} {selector}".format(discussion=self.root_selector, selector=selector), "{selector} is visible".format(selector=selector) ) self._find_within(selector).click() - def click_cancel_new_post(self): - self.click_element(".cancel") - EmptyPromise( - lambda: not self.is_new_post_opened(), - "New post closed" - ).fulfill() - - def click_new_post_button(self): - self.click_element(".new-post-btn") - EmptyPromise( - self.is_new_post_opened, - "New post opened" - ).fulfill() - @wait_for_js def _is_element_visible(self, selector): query = self._find_within(selector) return query.present and query.visible + def show_thread(self, thread_id): + """ + Clicks the link for the specified thread to show the detailed view. + """ + thread_selector = ".forum-nav-thread[data-id='{thread_id}'] .forum-nav-thread-link".format(thread_id=thread_id) + self._find_within(thread_selector).first.click() + self.thread_page = InlineDiscussionThreadPage(self.browser, thread_id) # pylint: disable=attribute-defined-outside-init + self.thread_page.wait_for_page() + class InlineDiscussionThreadPage(DiscussionThreadPage): + """ + Page object to manipulate an individual thread view in an inline discussion. + """ def __init__(self, browser, thread_id): super(InlineDiscussionThreadPage, self).__init__( browser, - "body.courseware .discussion-module #thread_{thread_id}".format(thread_id=thread_id) + ".discussion-module .discussion-article[data-id='{thread_id}']".format(thread_id=thread_id) ) - def expand(self): - """Clicks the link to expand the thread""" - self._find_within(".forum-thread-expand").first.click() - EmptyPromise( - lambda: bool(self.get_response_total_text()), - "Thread expanded" - ).fulfill() - def is_thread_anonymous(self): return not self.q(css=".posted-details > .username").present @@ -682,6 +716,7 @@ class DiscussionTabHomePage(CoursePage, DiscussionPageMixin): def __init__(self, browser, course_id): super(DiscussionTabHomePage, self).__init__(browser, course_id) self.url_path = "discussion/forum/" + self.root_selector = None def is_browser_on_page(self): return self.q(css=".discussion-body section.home-header").present @@ -733,18 +768,6 @@ class DiscussionTabHomePage(CoursePage, DiscussionPageMixin): "waiting for dismissed alerts to disappear" ).fulfill() - def click_new_post_button(self): - """ - Clicks the 'New Post' button. - """ - self.new_post_button.click() - EmptyPromise( - lambda: ( - self.new_post_form - ), - "New post action succeeded" - ).fulfill() - def click_element(self, selector): """ Clicks the element specified by selector @@ -752,22 +775,6 @@ class DiscussionTabHomePage(CoursePage, DiscussionPageMixin): element = self.q(css=selector) return element.click() - @property - def new_post_button(self): - """ - Returns the new post button. - """ - elements = self.q(css=".new-post-btn") - return elements.first if elements.visible and len(elements) == 1 else None - - @property - def new_post_form(self): - """ - Returns the new post form. - """ - elements = self.q(css=".forum-new-post-form") - return elements[0] if elements.visible and len(elements) == 1 else None - def set_new_post_editor_value(self, new_body): """ Set the Discussions new post editor (wmd) with the content in new_body diff --git a/common/test/acceptance/tests/discussion/helpers.py b/common/test/acceptance/tests/discussion/helpers.py index 0da35d8b21..84f5ff0d7a 100644 --- a/common/test/acceptance/tests/discussion/helpers.py +++ b/common/test/acceptance/tests/discussion/helpers.py @@ -37,6 +37,22 @@ class BaseDiscussionMixin(object): self.setup_thread_page(thread_id) return thread_id + def setup_multiple_threads(self, thread_count, **thread_kwargs): + """ + Set up multiple threads on the page by passing 'thread_count'. + """ + self.thread_ids = [] # pylint: disable=attribute-defined-outside-init + threads = [] # pylint: disable=attribute-defined-outside-init + for i in range(thread_count): + thread_id = "test_thread_{}_{}".format(i, uuid4().hex) + thread_body = "Dummy long text body." * 50 + threads.append( + Thread(id=thread_id, commentable_id=self.discussion_id, body=thread_body, **thread_kwargs), + ) + self.thread_ids.append(thread_id) + thread_fixture = MultipleThreadFixture(threads) + thread_fixture.push() + class CohortTestMixin(object): """ diff --git a/common/test/acceptance/tests/discussion/test_cohorts.py b/common/test/acceptance/tests/discussion/test_cohorts.py index dbdbac8237..fa765eb176 100644 --- a/common/test/acceptance/tests/discussion/test_cohorts.py +++ b/common/test/acceptance/tests/discussion/test_cohorts.py @@ -129,8 +129,8 @@ class InlineDiscussionTest(UniqueCourseTest): discussion_page = InlineDiscussionPage(self.browser, self.discussion_id) discussion_page.expand_discussion() self.assertEqual(discussion_page.get_num_displayed_threads(), 1) - self.thread_page = InlineDiscussionThreadPage(self.browser, thread_id) # pylint: disable=attribute-defined-outside-init - self.thread_page.expand() + discussion_page.show_thread(thread_id) + self.thread_page = discussion_page.thread_page # pylint: disable=attribute-defined-outside-init def refresh_thread_page(self, thread_id): self.browser.refresh() diff --git a/common/test/acceptance/tests/discussion/test_discussion.py b/common/test/acceptance/tests/discussion/test_discussion.py index 9cba009e4c..4e98377082 100644 --- a/common/test/acceptance/tests/discussion/test_discussion.py +++ b/common/test/acceptance/tests/discussion/test_discussion.py @@ -1031,41 +1031,8 @@ class InlineDiscussionTest(UniqueCourseTest, DiscussionResponsePaginationTestMix def setup_thread_page(self, thread_id): self.discussion_page.expand_discussion() - self.assertEqual(self.discussion_page.get_num_displayed_threads(), 1) - self.thread_page = InlineDiscussionThreadPage(self.browser, thread_id) # pylint: disable=attribute-defined-outside-init - self.thread_page.expand() - - def setup_multiple_inline_threads(self, thread_count): - """ - Set up multiple treads on the page by passing 'thread_count' - """ - threads = [] - for i in range(thread_count): - thread_id = "test_thread_{}_{}".format(i, uuid4().hex) - threads.append( - Thread(id=thread_id, commentable_id=self.discussion_id), - ) - self.thread_ids.append(thread_id) - thread_fixture = MultipleThreadFixture(threads) - thread_fixture.add_response( - Response(id="response1"), - [Comment(id="comment1", user_id="other"), Comment(id="comment2", user_id=self.user_id)], - threads[0] - ) - thread_fixture.push() - - def test_page_while_expanding_inline_discussion(self): - """ - Tests for the Inline Discussion page with multiple treads. Page should not focus 'thread-wrapper' - after loading responses. - """ - self.setup_multiple_inline_threads(thread_count=3) - self.discussion_page.expand_discussion() - thread_page = InlineDiscussionThreadPage(self.browser, self.thread_ids[0]) - thread_page.expand() - - # Check if 'thread-wrapper' is focused after expanding thread - self.assertFalse(thread_page.check_if_selector_is_focused(selector='.thread-wrapper')) + self.discussion_page.show_thread(thread_id) + self.thread_page = self.discussion_page.thread_page # pylint: disable=attribute-defined-outside-init @attr('a11y') def test_inline_a11y(self): @@ -1097,7 +1064,7 @@ class InlineDiscussionTest(UniqueCourseTest, DiscussionResponsePaginationTestMix thread = Thread(id=uuid4().hex, anonymous_to_peers=True, commentable_id=self.discussion_id) thread_fixture = SingleThreadViewFixture(thread) thread_fixture.push() - self.setup_thread_page(thread.get("id")) + self.setup_thread_page(thread.get("id")) # pylint: disable=no-member self.assertEqual(self.thread_page.is_thread_anonymous(), not is_staff) def test_anonymous_to_peers_threads_as_staff(self): @@ -1130,41 +1097,66 @@ class InlineDiscussionTest(UniqueCourseTest, DiscussionResponsePaginationTestMix Response(id="response1"), [Comment(id="comment1", user_id="other"), Comment(id="comment2", user_id=self.user_id)]) thread_fixture.push() - self.setup_thread_page(thread.get("id")) + self.setup_thread_page(thread.get("id")) # pylint: disable=no-member self.assertFalse(self.thread_page.has_add_response_button()) self.assertFalse(self.thread_page.is_element_visible("action-more")) def test_dual_discussion_xblock(self): """ Scenario: Two discussion xblocks in one unit shouldn't override their actions - Given that I'm on courseware page where there are two inline discussion - When I click on one discussion xblock new post button - Then it should add new post form of that xblock in DOM - And I should be shown new post form of that xblock - And I shouldn't be shown second discussion xblock new post form - And I click on second discussion xblock new post button - Then it should add new post form of second xblock in DOM - And I should be shown second discussion new post form - And I shouldn't be shown first discussion xblock new post form - And I have two new post form in the DOM - When I click back on first xblock new post button - And I should be shown new post form of that xblock - And I shouldn't be shown second discussion xblock new post form + Given that I'm on a courseware page where there are two inline discussion + When I click on the first discussion block's new post button + Then I should be shown only the new post form for the first block + When I click on the second discussion block's new post button + Then I should be shown both new post forms + When I cancel the first form + Then I should be shown only the new post form for the second block + When I cancel the second form + And I click on the first discussion block's new post button + Then I should be shown only the new post form for the first block + When I cancel the first form + Then I should be shown none of the forms """ self.discussion_page.wait_for_page() self.additional_discussion_page.wait_for_page() + + # Expand the first discussion, click to add a post self.discussion_page.expand_discussion() self.discussion_page.click_new_post_button() - with self.discussion_page.handle_alert(): - self.discussion_page.click_cancel_new_post() + + # Verify that only the first discussion's form is shown + self.assertIsNotNone(self.discussion_page.new_post_form) + self.assertIsNone(self.additional_discussion_page.new_post_form) + + # Expand the second discussion, click to add a post self.additional_discussion_page.expand_discussion() self.additional_discussion_page.click_new_post_button() - self.assertFalse(self.discussion_page._is_element_visible(".new-post-article")) - with self.additional_discussion_page.handle_alert(): - self.additional_discussion_page.click_cancel_new_post() - self.discussion_page.expand_discussion() + + # Verify that both discussion's forms are shown + self.assertIsNotNone(self.discussion_page.new_post_form) + self.assertIsNotNone(self.additional_discussion_page.new_post_form) + + # Cancel the first form + self.discussion_page.click_cancel_new_post() + + # Verify that only the second discussion's form is shown + self.assertIsNone(self.discussion_page.new_post_form) + self.assertIsNotNone(self.additional_discussion_page.new_post_form) + + # Cancel the second form and click to show the first one + self.additional_discussion_page.click_cancel_new_post() self.discussion_page.click_new_post_button() - self.assertFalse(self.additional_discussion_page._is_element_visible(".new-post-article")) + + # Verify that only the first discussion's form is shown + self.assertIsNotNone(self.discussion_page.new_post_form) + self.assertIsNone(self.additional_discussion_page.new_post_form) + + # Cancel the first form + self.discussion_page.click_cancel_new_post() + + # Verify that neither discussion's forms are shwon + self.assertIsNone(self.discussion_page.new_post_form) + self.assertIsNone(self.additional_discussion_page.new_post_form) @attr(shard=2) diff --git a/common/test/acceptance/tests/lms/test_teams.py b/common/test/acceptance/tests/lms/test_teams.py index 089da322c7..0f2b63e56b 100644 --- a/common/test/acceptance/tests/lms/test_teams.py +++ b/common/test/acceptance/tests/lms/test_teams.py @@ -1689,7 +1689,8 @@ class TeamPageTest(TeamsTabBase): thread = Thread( id="test_thread_{}".format(uuid4().hex), commentable_id=self.teams[0]['discussion_topic_id'], - body="Dummy text body." + body="Dummy text body.", + context="standalone", ) thread_fixture = MultipleThreadFixture([thread]) thread_fixture.push() @@ -1718,14 +1719,15 @@ class TeamPageTest(TeamsTabBase): thread = self.setup_thread() self.team_page.visit() self.assertEqual(self.team_page.discussion_id, self.teams[0]['discussion_topic_id']) - discussion = self.team_page.discussion_page - discussion.wait_for_page() - self.assertTrue(discussion.is_discussion_expanded()) - self.assertEqual(discussion.get_num_displayed_threads(), 1) - self.assertTrue(discussion.has_thread(thread['id'])) + discussion_page = self.team_page.discussion_page + discussion_page.wait_for_page() + self.assertTrue(discussion_page.is_discussion_expanded()) + self.assertEqual(discussion_page.get_num_displayed_threads(), 1) + discussion_page.show_thread(thread['id']) + thread_page = discussion_page.thread_page assertion = self.assertTrue if should_have_permission else self.assertFalse - assertion(discussion.q(css='.post-header-actions').present) - assertion(discussion.q(css='.add-response').present) + assertion(thread_page.q(css='.post-header-actions').present) + assertion(thread_page.q(css='.add-response').present) def test_discussion_on_my_team_page(self): """