index.spec.jsx 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  1. import {initializeOrg} from 'sentry-test/initializeOrg';
  2. import {act, render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary';
  3. import * as incidentActions from 'sentry/actionCreators/serviceIncidents';
  4. import SidebarContainer from 'sentry/components/sidebar';
  5. import ConfigStore from 'sentry/stores/configStore';
  6. import {PersistedStoreProvider} from 'sentry/stores/persistedStore';
  7. jest.mock('sentry/actionCreators/serviceIncidents');
  8. describe('Sidebar', function () {
  9. const {organization, router, routerContext} = initializeOrg();
  10. const broadcast = TestStubs.Broadcast();
  11. const user = TestStubs.User();
  12. const apiMocks = {};
  13. const location = {...router.location, ...{pathname: '/test/'}};
  14. const getElement = props => (
  15. <PersistedStoreProvider>
  16. <SidebarContainer organization={organization} location={location} {...props} />
  17. </PersistedStoreProvider>
  18. );
  19. const renderSidebar = props => render(getElement(props), {context: routerContext});
  20. beforeEach(function () {
  21. apiMocks.broadcasts = MockApiClient.addMockResponse({
  22. url: `/organizations/${organization.slug}/broadcasts/`,
  23. body: [broadcast],
  24. });
  25. apiMocks.broadcastsMarkAsSeen = MockApiClient.addMockResponse({
  26. url: '/broadcasts/',
  27. method: 'PUT',
  28. });
  29. apiMocks.sdkUpdates = MockApiClient.addMockResponse({
  30. url: `/organizations/${organization.slug}/sdk-updates/`,
  31. body: [],
  32. });
  33. MockApiClient.addMockResponse({
  34. url: `/organizations/${organization.slug}/client-state/`,
  35. body: {},
  36. });
  37. });
  38. it('renders', async function () {
  39. const {container} = renderSidebar();
  40. await waitFor(() => container);
  41. expect(screen.getByTestId('sidebar-dropdown')).toBeInTheDocument();
  42. });
  43. it('renders without org', function () {
  44. const {container} = renderSidebar({organization: null});
  45. // no org displays user details
  46. expect(screen.getByText(user.name)).toBeInTheDocument();
  47. expect(screen.getByText(user.email)).toBeInTheDocument();
  48. userEvent.click(screen.getByTestId('sidebar-dropdown'));
  49. expect(container).toSnapshot();
  50. });
  51. it('has can logout', async function () {
  52. const mock = MockApiClient.addMockResponse({
  53. url: '/auth/',
  54. method: 'DELETE',
  55. status: 204,
  56. });
  57. jest.spyOn(window.location, 'assign').mockImplementation(() => {});
  58. renderSidebar({
  59. organization: TestStubs.Organization({access: ['member:read']}),
  60. });
  61. userEvent.click(screen.getByTestId('sidebar-dropdown'));
  62. userEvent.click(screen.getByTestId('sidebar-signout'));
  63. await waitFor(() => expect(mock).toHaveBeenCalled());
  64. expect(window.location.assign).toHaveBeenCalledWith('/auth/login/');
  65. window.location.assign.mockRestore();
  66. });
  67. it('can toggle help menu', async function () {
  68. const {container} = renderSidebar();
  69. await waitFor(() => container);
  70. userEvent.click(screen.getByText('Help'));
  71. expect(screen.getByText('Visit Help Center')).toBeInTheDocument();
  72. expect(container).toSnapshot();
  73. });
  74. describe('SidebarDropdown', function () {
  75. it('can open Sidebar org/name dropdown menu', async function () {
  76. const {container} = renderSidebar();
  77. await waitFor(() => container);
  78. userEvent.click(screen.getByTestId('sidebar-dropdown'));
  79. const orgSettingsLink = screen.getByText('Organization settings');
  80. expect(orgSettingsLink).toBeInTheDocument();
  81. expect(container).toSnapshot();
  82. });
  83. it('has link to Members settings with `member:write`', async function () {
  84. const {container} = renderSidebar({
  85. organization: TestStubs.Organization({access: ['member:read']}),
  86. });
  87. await waitFor(() => container);
  88. userEvent.click(screen.getByTestId('sidebar-dropdown'));
  89. expect(screen.getByText('Members')).toBeInTheDocument();
  90. });
  91. it('can open "Switch Organization" sub-menu', async function () {
  92. act(() => void ConfigStore.set('features', new Set(['organizations:create'])));
  93. const {container} = renderSidebar();
  94. await waitFor(() => container);
  95. userEvent.click(screen.getByTestId('sidebar-dropdown'));
  96. jest.useFakeTimers();
  97. userEvent.type(screen.getByText('Switch organization'), '{enter}');
  98. act(() => jest.advanceTimersByTime(500));
  99. jest.useRealTimers();
  100. const createOrg = screen.getByText('Create a new organization');
  101. expect(createOrg).toBeInTheDocument();
  102. expect(container).toSnapshot();
  103. });
  104. });
  105. describe('SidebarPanel', function () {
  106. it('hides when path changes', async function () {
  107. const {rerender} = renderSidebar();
  108. userEvent.click(screen.getByText("What's new"));
  109. expect(await screen.findByRole('dialog')).toBeInTheDocument();
  110. expect(screen.getByText("What's new in Sentry")).toBeInTheDocument();
  111. rerender(getElement({location: {...router.location, pathname: 'new-path-name'}}));
  112. expect(screen.queryByText("What's new in Sentry")).not.toBeInTheDocument();
  113. await tick();
  114. });
  115. it('can have onboarding feature', async function () {
  116. renderSidebar({
  117. organization: {...organization, features: ['onboarding']},
  118. });
  119. const quickStart = screen.getByText('Quick Start');
  120. expect(quickStart).toBeInTheDocument();
  121. userEvent.click(quickStart);
  122. expect(await screen.findByRole('dialog')).toBeInTheDocument();
  123. expect(screen.getByText('Capture your first error')).toBeInTheDocument();
  124. userEvent.click(quickStart);
  125. expect(screen.queryByText('Capture your first error')).not.toBeInTheDocument();
  126. await tick();
  127. });
  128. it('displays empty panel when there are no Broadcasts', async function () {
  129. MockApiClient.addMockResponse({
  130. url: `/organizations/${organization.slug}/broadcasts/`,
  131. body: [],
  132. });
  133. renderSidebar();
  134. userEvent.click(screen.getByText("What's new"));
  135. expect(await screen.findByRole('dialog')).toBeInTheDocument();
  136. expect(screen.getByText("What's new in Sentry")).toBeInTheDocument();
  137. expect(
  138. screen.getByText('No recent updates from the Sentry team.')
  139. ).toBeInTheDocument();
  140. // Close the sidebar
  141. userEvent.click(screen.getByText("What's new"));
  142. expect(screen.queryByText("What's new in Sentry")).not.toBeInTheDocument();
  143. await tick();
  144. });
  145. it('can display Broadcasts panel and mark as seen', async function () {
  146. jest.useFakeTimers();
  147. const {container} = renderSidebar();
  148. expect(apiMocks.broadcasts).toHaveBeenCalled();
  149. userEvent.click(screen.getByText("What's new"));
  150. expect(await screen.findByRole('dialog')).toBeInTheDocument();
  151. expect(screen.getByText("What's new in Sentry")).toBeInTheDocument();
  152. const broadcastTitle = screen.getByText(broadcast.title);
  153. expect(broadcastTitle).toBeInTheDocument();
  154. expect(container).toSnapshot();
  155. // Should mark as seen after a delay
  156. act(() => jest.advanceTimersByTime(2000));
  157. expect(apiMocks.broadcastsMarkAsSeen).toHaveBeenCalledWith(
  158. '/broadcasts/',
  159. expect.objectContaining({
  160. data: {hasSeen: '1'},
  161. query: {id: ['8']},
  162. })
  163. );
  164. jest.useRealTimers();
  165. // Close the sidebar
  166. userEvent.click(screen.getByText("What's new"));
  167. expect(screen.queryByText("What's new in Sentry")).not.toBeInTheDocument();
  168. await tick();
  169. });
  170. it('can unmount Sidebar (and Broadcasts) and kills Broadcast timers', async function () {
  171. jest.useFakeTimers();
  172. const {unmount} = renderSidebar();
  173. // This will start timer to mark as seen
  174. userEvent.click(screen.getByRole('link', {name: "What's new"}));
  175. expect(await screen.findByText("What's new in Sentry")).toBeInTheDocument();
  176. act(() => jest.advanceTimersByTime(500));
  177. // Unmounting will cancel timers
  178. unmount();
  179. // This advances timers enough so that mark as seen should be called if
  180. // it wasn't unmounted
  181. act(() => jest.advanceTimersByTime(600));
  182. expect(apiMocks.broadcastsMarkAsSeen).not.toHaveBeenCalled();
  183. jest.useRealTimers();
  184. });
  185. it('can show Incidents in Sidebar Panel', async function () {
  186. incidentActions.loadIncidents = jest.fn(() => ({
  187. incidents: [TestStubs.ServiceIncident()],
  188. }));
  189. const {container} = renderSidebar();
  190. userEvent.click(await screen.findByText('Service status'));
  191. await screen.findByText('Recent service updates');
  192. expect(container).toSnapshot();
  193. });
  194. });
  195. it('can toggle collapsed state', async function () {
  196. const container = renderSidebar();
  197. await waitFor(() => container);
  198. expect(screen.getByText(user.name)).toBeInTheDocument();
  199. expect(screen.getByText(organization.name)).toBeInTheDocument();
  200. userEvent.click(screen.getByTestId('sidebar-collapse'));
  201. // Check that the organization name is no longer visible
  202. expect(screen.queryByText(organization.name)).not.toBeInTheDocument();
  203. // Un-collapse he sidebar and make sure the org name is visible again
  204. userEvent.click(screen.getByTestId('sidebar-collapse'));
  205. expect(await screen.findByText(organization.name)).toBeInTheDocument();
  206. });
  207. });