index.spec.jsx 9.4 KB

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