From a00a0736715e5bfb512e082f5e38a7a429754a89 Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Fri, 24 Apr 2020 11:41:19 -0700 Subject: [PATCH] tests(acceptance): Add acceptance tests for global selection header (#18452) This adds some acceptance tests for global selection header, some tests are commented out as they are bugs that need to be fixed. --- .../globalSelectionHeader.tsx | 1 + src/sentry/utils/pytest/selenium.py | 31 +- .../page_objects/global_selection.py | 50 ++++ .../acceptance/page_objects/issue_details.py | 15 + tests/acceptance/page_objects/issue_list.py | 11 + ...st_organization_global_selection_header.py | 279 +++++++++++++++++- 6 files changed, 380 insertions(+), 7 deletions(-) create mode 100644 tests/acceptance/page_objects/global_selection.py diff --git a/src/sentry/static/sentry/app/components/organizations/globalSelectionHeader/globalSelectionHeader.tsx b/src/sentry/static/sentry/app/components/organizations/globalSelectionHeader/globalSelectionHeader.tsx index 12e80a0103ecab..6d44db30b902a1 100644 --- a/src/sentry/static/sentry/app/components/organizations/globalSelectionHeader/globalSelectionHeader.tsx +++ b/src/sentry/static/sentry/app/components/organizations/globalSelectionHeader/globalSelectionHeader.tsx @@ -614,6 +614,7 @@ class GlobalSelectionHeader extends React.Component { diff --git a/src/sentry/utils/pytest/selenium.py b/src/sentry/utils/pytest/selenium.py index 0905e962409d38..8e936ea26d49d1 100644 --- a/src/sentry/utils/pytest/selenium.py +++ b/src/sentry/utils/pytest/selenium.py @@ -69,12 +69,17 @@ def delete(self, path, *args, **kwargs): self._has_initialized_cookie_store = True return self - def element(self, selector): + def element(self, selector=None, xpath=None): """ Get an element from the page. This method will wait for the element to show up. """ - self.wait_until(selector) - return self.driver.find_element_by_css_selector(selector) + + if xpath is not None: + self.wait_until(xpath=xpath) + return self.driver.find_element_by_xpath(xpath) + else: + self.wait_until(selector) + return self.driver.find_element_by_css_selector(selector) def element_exists(self, selector): """ @@ -100,8 +105,8 @@ def element_exists_by_aria_label(self, selector): """ return self.element_exists('[aria-label="%s"]' % (selector)) - def click(self, selector): - self.element(selector).click() + def click(self, selector=None, xpath=None): + self.element(selector, xpath=xpath).click() def click_when_visible(self, selector=None, timeout=3): """ @@ -217,6 +222,22 @@ def snapshot(self, name): self.percy.snapshot(name=name) return self + def get_local_storage_items(self): + """ + Retrieve all items in local storage + """ + + return self.driver.execute_script( + "Object.fromEntries(Object.entries(window.localStorage));" + ) + + def get_local_storage_item(self, key): + """ + Retrieve key from local storage, this will fail if you use single quotes in your keys. + """ + + return self.driver.execute_script(u"window.localStorage.getItem('{}')".format(key)) + def save_cookie( self, name, diff --git a/tests/acceptance/page_objects/global_selection.py b/tests/acceptance/page_objects/global_selection.py new file mode 100644 index 00000000000000..c7d62117ae0492 --- /dev/null +++ b/tests/acceptance/page_objects/global_selection.py @@ -0,0 +1,50 @@ +from __future__ import absolute_import + +from .base import BasePage + + +class GlobalSelectionPage(BasePage): + def __init__(self, browser, client): + super(GlobalSelectionPage, self).__init__(browser) + self.client = client + + def get_selected_project_slug(self): + return self.browser.element('[data-test-id="global-header-project-selector"]').text + + def get_selected_environment(self): + return self.browser.element('[data-test-id="global-header-environment-selector"]').text + + def go_back_to_issues(self): + self.browser.click('[data-test-id="back-to-issues"]') + + def open_project_selector(self): + self.browser.click('[data-test-id="global-header-project-selector"]') + + def select_project_by_slug(self, slug): + project_item_selector = u'//*[@data-test-id="badge-display-name" and text()="{}"]'.format( + slug + ) + + self.open_project_selector() + self.browser.wait_until(xpath=project_item_selector) + self.browser.click(xpath=project_item_selector) + + def open_environment_selector(self): + self.browser.click('[data-test-id="global-header-environment-selector"]') + + def select_environment(self, environment): + environment_path = u'//*[text()="{}"]'.format(environment) + + self.open_project_selector() + self.browser.wait_until(xpath=environment_path) + self.browser.click(xpath=environment_path) + + def open_date_selector(self): + self.browser.click('[data-test-id="global-header-timerange-selector"]') + + def select_date(self, date): + date_path = u'//*[text()="{}"]'.format(date) + + self.open_project_selector() + self.browser.wait_until(xpath=date_path) + self.browser.click(xpath=date_path) diff --git a/tests/acceptance/page_objects/issue_details.py b/tests/acceptance/page_objects/issue_details.py index a8592bfb43f1ea..00f966f0c5bb2b 100644 --- a/tests/acceptance/page_objects/issue_details.py +++ b/tests/acceptance/page_objects/issue_details.py @@ -1,23 +1,38 @@ from __future__ import absolute_import from .base import BasePage +from .global_selection import GlobalSelectionPage class IssueDetailsPage(BasePage): def __init__(self, browser, client): super(IssueDetailsPage, self).__init__(browser) self.client = client + self.global_selection = GlobalSelectionPage(browser, client) def visit_issue(self, org, groupid): self.dismiss_assistant() self.browser.get(u"/organizations/{}/issues/{}/".format(org, groupid)) self.wait_until_loaded() + def visit_issue_in_environment(self, org, groupid, environment): + self.dismiss_assistant() + self.browser.get( + u"/organizations/{}/issues/{}/?environment={}".format(org, groupid, environment) + ) + self.browser.wait_until(".group-detail") + def visit_tag_values(self, org, groupid, tag): self.dismiss_assistant() self.browser.get(u"/organizations/{}/issues/{}/tags/{}".format(org, groupid, tag)) self.browser.wait_until_not(".loading-indicator") + def get_environment(self): + return self.browser.find_element_by_css_selector('[data-test-id="env-label"').text.lower() + + def go_back_to_issues(self): + self.global_selection.go_back_to_issues() + def api_issue_get(self, groupid): return self.client.get(u"/api/0/issues/{}/".format(groupid)) diff --git a/tests/acceptance/page_objects/issue_list.py b/tests/acceptance/page_objects/issue_list.py index 066cf6da4c4bae..7833a7be8dc812 100644 --- a/tests/acceptance/page_objects/issue_list.py +++ b/tests/acceptance/page_objects/issue_list.py @@ -1,12 +1,15 @@ from __future__ import absolute_import from .base import BasePage +from .global_selection import GlobalSelectionPage +from .issue_details import IssueDetailsPage class IssueListPage(BasePage): def __init__(self, browser, client): super(IssueListPage, self).__init__(browser) self.client = client + self.global_selection = GlobalSelectionPage(browser, client) def visit_issue_list(self, org, query=""): self.dismiss_assistant() @@ -19,6 +22,11 @@ def wait_for_stream(self): def select_issue(self, position): self.browser.click(u'[data-test-id="group"]:nth-child({})'.format(position)) + def navigate_to_issue(self, position): + self.browser.click(u'[data-test-id="group"]:nth-child({}) a'.format(position)) + self.browser.wait_until(".group-detail") + self.issue_details = IssueDetailsPage(self.browser, self.client) + def resolve_issues(self): self.browser.click('[aria-label="Resolve"]') self.browser.click('[data-test-id="confirm-button"]') @@ -26,5 +34,8 @@ def resolve_issues(self): def wait_for_resolved_issue(self): self.browser.wait_until('[data-test-id="resolved-issue"]') + def wait_for_issue(self): + self.browser.wait_until('[data-test-id="group"]') + def find_resolved_issues(self): return self.browser.find_elements_by_css_selector('[data-test-id="resolved-issue"]') diff --git a/tests/acceptance/test_organization_global_selection_header.py b/tests/acceptance/test_organization_global_selection_header.py index 6a81c16f2f8afa..f5083f93c206e6 100644 --- a/tests/acceptance/test_organization_global_selection_header.py +++ b/tests/acceptance/test_organization_global_selection_header.py @@ -1,12 +1,19 @@ from __future__ import absolute_import +from datetime import datetime import six +import pytz from django.utils import timezone from sentry.testutils import AcceptanceTestCase, SnubaTestCase +from sentry.testutils.helpers.datetime import iso_format, before_now +from sentry.utils.compat.mock import patch from tests.acceptance.page_objects.issue_list import IssueListPage +from tests.acceptance.page_objects.issue_details import IssueDetailsPage + +event_time = before_now(days=3).replace(tzinfo=pytz.utc) class OrganizationGlobalHeaderTest(AcceptanceTestCase, SnubaTestCase): @@ -32,13 +39,37 @@ def setUp(self): self.create_environment(name="production", project=self.project_1) self.create_environment(name="visible", project=self.project_1, is_hidden=False) self.create_environment(name="not visible", project=self.project_1, is_hidden=True) + self.create_environment(name="dev", project=self.project_2) + self.create_environment(name="prod", project=self.project_2) self.login_as(self.user) - self.page = IssueListPage(self.browser, self.client) + self.issues_list = IssueListPage(self.browser, self.client) + self.issue_details = IssueDetailsPage(self.browser, self.client) + + def create_issues(self): + self.issue_1 = self.store_event( + data={ + "event_id": "a" * 32, + "message": "oh no", + "timestamp": iso_format(event_time), + "fingerprint": ["group-1"], + }, + project_id=self.project_1.id, + ) + self.issue_2 = self.store_event( + data={ + "event_id": "b" * 32, + "message": "oh snap", + "timestamp": iso_format(event_time), + "fingerprint": ["group-2"], + "environment": "prod", + }, + project_id=self.project_2.id, + ) def test_global_selection_header_dropdown(self): self.project.update(first_event=timezone.now()) - self.page.visit_issue_list( + self.issues_list.visit_issue_list( self.org.slug, query="?query=assigned%3Ame&project=" + six.text_type(self.project_1.id) ) self.browser.wait_until_test_id("awaiting-events") @@ -51,3 +82,247 @@ def test_global_selection_header_dropdown(self): self.browser.click('[data-test-id="global-header-timerange-selector"]') self.browser.snapshot("globalSelectionHeader - timerange selector") + + def test_global_selection_header_loads_with_correct_project(self): + """ + Global Selection Header should: + 1) load project from URL if it exists + 2) enforce a single project if loading issues list with no project in URL + a) last selected project via local storage if it exists + b) otherwise need to just select first project + """ + self.create_issues() + # No project id in URL, selects first project + self.issues_list.visit_issue_list(self.org.slug) + assert u"project={}".format(self.project_1.id) in self.browser.current_url + assert self.issues_list.global_selection.get_selected_project_slug() == self.project_1.slug + + # Uses project id in URL + self.issues_list.visit_issue_list( + self.org.slug, query=u"?project={}".format(self.project_2.id) + ) + assert u"project={}".format(self.project_2.id) in self.browser.current_url + assert self.issues_list.global_selection.get_selected_project_slug() == self.project_2.slug + + # reloads page with no project id in URL, selects first project + self.issues_list.visit_issue_list(self.org.slug) + assert u"project={}".format(self.project_1.id) in self.browser.current_url + assert self.issues_list.global_selection.get_selected_project_slug() == self.project_1.slug + + # can select a different project + self.issues_list.global_selection.select_project_by_slug(self.project_3.slug) + self.issues_list.wait_until_loaded() + assert u"project={}".format(self.project_3.id) in self.browser.current_url + assert self.issues_list.global_selection.get_selected_project_slug() == self.project_3.slug + + # reloading page with no project id in URL after previously + # selecting an explicit project should load previously selected project + # from local storage + # TODO check environment as well + # FIXME below is currently broken + # self.issues_list.visit_issue_list( + # self.org.slug + # ) + # self.issues_list.wait_until_loaded() + # assert u"project={}".format(self.project_3.id) in self.browser.current_url + + def test_global_selection_header_loads_with_correct_project_with_multi_project(self): + """ + Global Selection Header should: + 1) load project from URL if it exists + 2) load last selected projects via local storage if it exists + 3) otherwise can search within "my projects" + """ + with self.feature("organizations:global-views"): + self.create_issues() + # No project id in URL, is "my projects" + self.issues_list.visit_issue_list(self.org.slug) + assert u"project=" not in self.browser.current_url + assert self.issues_list.global_selection.get_selected_project_slug() == "My Projects" + assert ( + self.browser.get_local_storage_item(u"global-selection:{}".format(self.org.slug)) + is None + ) + + # Uses project id in URL + self.issues_list.visit_issue_list( + self.org.slug, query=u"?project={}".format(self.project_2.id) + ) + assert u"project={}".format(self.project_2.id) in self.browser.current_url + assert ( + self.issues_list.global_selection.get_selected_project_slug() == self.project_2.slug + ) + + # FIXME(billy): This is a bug, should not be in local storage + # should not be in local storage + # assert self.browser.get_local_storage_item(u"global-selection:{}".format(self.org.slug)) is None + + # reloads page with no project id in URL, remains "My Projects" because + # there has been no explicit project selection via UI + self.issues_list.visit_issue_list(self.org.slug) + assert u"project=" not in self.browser.current_url + # FIXME + # assert self.issues_list.global_selection.get_selected_project_slug() == "My Projects" + + # can select a different project + self.issues_list.global_selection.select_project_by_slug(self.project_3.slug) + self.issues_list.wait_until_loaded() + assert u"project={}".format(self.project_3.id) in self.browser.current_url + assert ( + self.issues_list.global_selection.get_selected_project_slug() == self.project_3.slug + ) + + # reloading page with no project id in URL after previously + # selecting an explicit project should load previously selected project + # from local storage + self.issues_list.visit_issue_list(self.org.slug) + self.issues_list.wait_until_loaded() + # TODO check environment as well + # FIXME: This is current broken and is a bug + # assert u"project={}".format(self.project_3.id) in self.browser.current_url + assert ( + self.issues_list.global_selection.get_selected_project_slug() == self.project_3.slug + ) + + @patch("django.utils.timezone.now") + def test_issues_list_to_details_and_back_with_all_projects(self, mock_now): + """ + If user has access to the `global-views` feature, which allows selecting multiple projects, + they should be able to visit issues list with no project in URL and list issues + for all projects they are members of. + + They should also be able to open an issue and then navigate back to still see + "My Projects" in issues list. + """ + with self.feature("organizations:global-views"): + mock_now.return_value = datetime.utcnow().replace(tzinfo=pytz.utc) + self.create_issues() + self.issues_list.visit_issue_list(self.org.slug) + self.issues_list.wait_for_issue() + + assert u"project=" not in self.browser.current_url + assert self.issues_list.global_selection.get_selected_project_slug() == "My Projects" + + # select the issue + self.issues_list.navigate_to_issue(1) + + # going back to issues list should not have the issue's project id in url + self.issues_list.issue_details.go_back_to_issues() + self.issues_list.wait_for_issue() + + # project id should remain *NOT* in URL + assert u"project=" not in self.browser.current_url + assert self.issues_list.global_selection.get_selected_project_slug() == "My Projects" + + # can select a different project + self.issues_list.global_selection.select_project_by_slug(self.project_3.slug) + self.issues_list.wait_until_loaded() + assert u"project={}".format(self.project_3.id) in self.browser.current_url + assert ( + self.issues_list.global_selection.get_selected_project_slug() == self.project_3.slug + ) + + @patch("django.utils.timezone.now") + def test_issues_list_to_details_and_back_with_initial_project(self, mock_now): + """ + If user has a project defined in URL, if they visit an issue and then + return back to issues list, that project id should still exist in URL + """ + mock_now.return_value = datetime.utcnow().replace(tzinfo=pytz.utc) + self.create_issues() + self.issues_list.visit_issue_list( + self.org.slug, query=u"?project={}".format(self.project_2.id) + ) + self.issues_list.wait_for_issue() + + assert u"project={}".format(self.project_2.id) in self.browser.current_url + assert self.issues_list.global_selection.get_selected_project_slug() == self.project_2.slug + + # select the issue + self.issues_list.navigate_to_issue(1) + + # project id should remain in URL + assert u"project={}".format(self.project_2.id) in self.browser.current_url + + # going back to issues list should keep project in URL + self.issues_list.issue_details.go_back_to_issues() + self.issues_list.wait_for_issue() + + # project id should remain in URL + assert u"project={}".format(self.project_2.id) in self.browser.current_url + + # can select a different project + self.issues_list.global_selection.select_project_by_slug(self.project_3.slug) + self.issues_list.wait_until_loaded() + assert u"project={}".format(self.project_3.id) in self.browser.current_url + assert self.issues_list.global_selection.get_selected_project_slug() == self.project_3.slug + + @patch("django.utils.timezone.now") + def test_issue_details_to_stream_with_initial_env_no_project(self, mock_now): + """ + Visiting issue details directly with no project but with an environment defined in URL. + When navigating back to issues stream, should keep environment and project in context. + """ + + mock_now.return_value = datetime.utcnow().replace(tzinfo=pytz.utc) + self.create_issues() + self.issue_details.visit_issue_in_environment(self.org.slug, self.issue_2.group.id, "prod") + + # Make sure issue's project is in URL and in header + assert u"project={}".format(self.project_2.id) in self.browser.current_url + assert self.issues_list.global_selection.get_selected_project_slug() == self.project_2.slug + + # environment should be in URL and header + assert u"environment=prod" in self.browser.current_url + assert self.issue_details.global_selection.get_selected_environment() == "prod" + + # going back to issues list should keep project and environment in URL + self.issue_details.go_back_to_issues() + self.issues_list.wait_for_issue() + + # project id should remain in URL + assert u"project={}".format(self.project_2.id) in self.browser.current_url + assert u"environment=prod" in self.browser.current_url + assert self.issues_list.global_selection.get_selected_project_slug() == self.project_2.slug + assert self.issue_details.global_selection.get_selected_environment() == "prod" + + @patch("django.utils.timezone.now") + def test_issue_details_to_stream_with_initial_env_no_project_with_multi_project_feature( + self, mock_now + ): + """ + Visiting issue details directly with no project but with an environment defined in URL. + When navigating back to issues stream, should keep environment and project in context. + """ + + with self.feature("organizations:global-views"): + mock_now.return_value = datetime.utcnow().replace(tzinfo=pytz.utc) + self.create_issues() + self.issue_details.visit_issue_in_environment( + self.org.slug, self.issue_2.group.id, "prod" + ) + + # Make sure issue's project is in URL and in header + assert u"project={}".format(self.project_2.id) in self.browser.current_url + assert ( + self.issues_list.global_selection.get_selected_project_slug() == self.project_2.slug + ) + + # environment should be in URL and header + assert u"environment=prod" in self.browser.current_url + assert self.issue_details.global_selection.get_selected_environment() == "prod" + + # can change environment so that when you navigate back to issues stream, + # it keeps environment as selected + + # going back to issues list should keep project and environment in URL + self.issue_details.go_back_to_issues() + self.issues_list.wait_for_issue() + + # project id should remain in URL + assert u"project={}".format(self.project_2.id) in self.browser.current_url + assert u"environment=prod" in self.browser.current_url + assert ( + self.issues_list.global_selection.get_selected_project_slug() == self.project_2.slug + ) + assert self.issue_details.global_selection.get_selected_environment() == "prod"