index.spec.jsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335
  1. import {mountWithTheme} from 'sentry-test/enzyme';
  2. import * as incidentActions from 'app/actionCreators/serviceIncidents';
  3. import SidebarContainer from 'app/components/sidebar';
  4. import ConfigStore from 'app/stores/configStore';
  5. jest.mock('app/actionCreators/serviceIncidents');
  6. describe('Sidebar', function () {
  7. let wrapper;
  8. const routerContext = TestStubs.routerContext();
  9. const {organization, router} = routerContext.context;
  10. const user = TestStubs.User();
  11. const apiMocks = {};
  12. const createWrapper = props =>
  13. mountWithTheme(
  14. <SidebarContainer
  15. organization={organization}
  16. user={user}
  17. router={router}
  18. location={{...router.location, ...{pathname: '/test/'}}}
  19. {...props}
  20. />,
  21. routerContext
  22. );
  23. beforeEach(function () {
  24. apiMocks.broadcasts = MockApiClient.addMockResponse({
  25. url: `/organizations/${organization.slug}/broadcasts/`,
  26. body: [TestStubs.Broadcast()],
  27. });
  28. apiMocks.broadcastsMarkAsSeen = MockApiClient.addMockResponse({
  29. url: '/broadcasts/',
  30. method: 'PUT',
  31. });
  32. apiMocks.sdkUpdates = MockApiClient.addMockResponse({
  33. url: `/organizations/${organization.slug}/sdk-updates/`,
  34. body: [],
  35. });
  36. });
  37. it('renders', function () {
  38. wrapper = mountWithTheme(
  39. <SidebarContainer organization={organization} user={user} router={router} />,
  40. TestStubs.routerContext()
  41. );
  42. expect(wrapper.find('SidebarWrapper')).toHaveLength(1);
  43. });
  44. it('renders without org and router', function () {
  45. wrapper = createWrapper({
  46. organization: null,
  47. router: null,
  48. });
  49. // no org displays user details
  50. expect(wrapper.find('OrgOrUserName').text()).toContain(user.name);
  51. expect(wrapper.find('UserNameOrEmail').text()).toContain(user.email);
  52. wrapper.find('SidebarDropdownActor').simulate('click');
  53. expect(wrapper).toSnapshot();
  54. });
  55. it('can toggle collapsed state', async function () {
  56. wrapper = mountWithTheme(
  57. <SidebarContainer organization={organization} user={user} router={router} />,
  58. routerContext
  59. );
  60. expect(wrapper.find('OrgOrUserName').text()).toContain(organization.name);
  61. expect(wrapper.find('UserNameOrEmail').text()).toContain(user.name);
  62. wrapper.find('SidebarCollapseItem StyledSidebarItem').simulate('click');
  63. await tick();
  64. wrapper.update();
  65. // Because of HoCs, we can't access the collapsed prop
  66. // Instead check for `SidebarItemLabel` which doesn't exist in collapsed state
  67. expect(wrapper.find('SidebarItemLabel')).toHaveLength(0);
  68. wrapper.find('SidebarCollapseItem StyledSidebarItem').simulate('click');
  69. await tick();
  70. wrapper.update();
  71. expect(wrapper.find('SidebarItemLabel').length).toBeGreaterThan(0);
  72. });
  73. it('can have onboarding feature', async function () {
  74. wrapper = mountWithTheme(
  75. <SidebarContainer
  76. organization={{...organization, features: ['onboarding']}}
  77. user={user}
  78. router={router}
  79. />,
  80. routerContext
  81. );
  82. expect(wrapper.find('OnboardingStatus ProgressRing')).toHaveLength(1);
  83. wrapper.find('OnboardingStatus ProgressRing').simulate('click');
  84. await tick();
  85. wrapper.update();
  86. expect(wrapper.find('OnboardingStatus TaskSidebarPanel').exists()).toBe(true);
  87. });
  88. describe('SidebarHelp', function () {
  89. it('can toggle help menu', function () {
  90. wrapper = createWrapper();
  91. wrapper.find('HelpActor').simulate('click');
  92. const menu = wrapper.find('HelpMenu');
  93. expect(menu).toHaveLength(1);
  94. expect(wrapper).toSnapshot();
  95. expect(menu.find('SidebarMenuItem')).toHaveLength(2);
  96. wrapper.find('HelpActor').simulate('click');
  97. expect(wrapper.find('HelpMenu')).toHaveLength(0);
  98. });
  99. });
  100. describe('SidebarDropdown', function () {
  101. it('can open Sidebar org/name dropdown menu', function () {
  102. wrapper = createWrapper();
  103. wrapper.find('SidebarDropdownActor').simulate('click');
  104. expect(wrapper.find('OrgAndUserMenu')).toHaveLength(1);
  105. expect(wrapper).toSnapshot();
  106. });
  107. it('has link to Members settings with `member:write`', function () {
  108. let org = TestStubs.Organization();
  109. org = {
  110. ...org,
  111. access: [...org.access, 'member:read'],
  112. };
  113. wrapper = createWrapper({
  114. organization: org,
  115. });
  116. wrapper.find('SidebarDropdownActor').simulate('click');
  117. expect(wrapper.find('OrgAndUserMenu')).toHaveLength(1);
  118. expect(
  119. wrapper.find('SidebarMenuItem[to="/settings/org-slug/members/"]')
  120. ).toHaveLength(1);
  121. });
  122. it('can open "Switch Organization" sub-menu', function () {
  123. ConfigStore.set('features', new Set(['organizations:create']));
  124. jest.useFakeTimers();
  125. wrapper = createWrapper();
  126. wrapper.find('SidebarDropdownActor').simulate('click');
  127. wrapper.find('SwitchOrganizationMenuActor').simulate('mouseEnter');
  128. jest.advanceTimersByTime(500);
  129. wrapper.update();
  130. expect(wrapper.find('SwitchOrganizationMenu')).toHaveLength(1);
  131. expect(wrapper).toSnapshot();
  132. jest.useRealTimers();
  133. });
  134. it('has can logout', async function () {
  135. const mock = MockApiClient.addMockResponse({
  136. url: '/auth/',
  137. method: 'DELETE',
  138. status: 204,
  139. });
  140. jest.spyOn(window.location, 'assign').mockImplementation(() => {});
  141. let org = TestStubs.Organization();
  142. org = {
  143. ...org,
  144. access: [...org.access, 'member:read'],
  145. };
  146. wrapper = createWrapper({
  147. organization: org,
  148. user: TestStubs.User(),
  149. });
  150. wrapper.find('SidebarDropdownActor').simulate('click');
  151. wrapper.find('SidebarMenuItem[data-test-id="sidebarSignout"]').simulate('click');
  152. expect(mock).toHaveBeenCalled();
  153. await tick();
  154. expect(window.location.assign).toHaveBeenCalledWith('/auth/login/');
  155. window.location.assign.mockRestore();
  156. });
  157. });
  158. describe('SidebarPanel', function () {
  159. it('displays empty panel when there are no Broadcasts', async function () {
  160. MockApiClient.addMockResponse({
  161. url: `/organizations/${organization.slug}/broadcasts/`,
  162. body: [],
  163. });
  164. wrapper = createWrapper();
  165. wrapper.find('Broadcasts SidebarItem').simulate('click');
  166. await tick();
  167. wrapper.update();
  168. expect(wrapper.find('SidebarPanel')).toHaveLength(1);
  169. expect(wrapper.find('SidebarPanelItem')).toHaveLength(0);
  170. expect(wrapper.find('SidebarPanelEmpty')).toHaveLength(1);
  171. // Close the sidebar
  172. wrapper.find('Broadcasts SidebarItem').simulate('click');
  173. await tick();
  174. wrapper.update();
  175. });
  176. it('can display Broadcasts panel and mark as seen', async function () {
  177. jest.useFakeTimers();
  178. wrapper = createWrapper();
  179. expect(apiMocks.broadcasts).toHaveBeenCalled();
  180. wrapper.find('Broadcasts SidebarItem').simulate('click');
  181. // XXX: Need to do this for reflux since we're using fake timers
  182. jest.advanceTimersByTime(0);
  183. await Promise.resolve();
  184. wrapper.update();
  185. expect(wrapper.find('SidebarPanel')).toHaveLength(1);
  186. expect(wrapper.find('SidebarPanelItem')).toHaveLength(1);
  187. expect(wrapper.find('SidebarPanelItem').prop('hasSeen')).toBe(false);
  188. expect(wrapper.find('SidebarPanelItem')).toSnapshot();
  189. // Should mark as seen after a delay
  190. jest.advanceTimersByTime(2000);
  191. expect(apiMocks.broadcastsMarkAsSeen).toHaveBeenCalledWith(
  192. '/broadcasts/',
  193. expect.objectContaining({
  194. data: {
  195. hasSeen: '1',
  196. },
  197. query: {
  198. id: ['8'],
  199. },
  200. })
  201. );
  202. jest.useRealTimers();
  203. // Close the sidebar
  204. wrapper.find('Broadcasts SidebarItem').simulate('click');
  205. });
  206. it('can toggle display of Broadcasts SidebarPanel', async function () {
  207. wrapper = createWrapper();
  208. wrapper.update();
  209. // Show Broadcasts Panel
  210. wrapper.find('Broadcasts SidebarItem').simulate('click');
  211. await tick();
  212. wrapper.update();
  213. expect(wrapper.find('SidebarPanel')).toHaveLength(1);
  214. // Hide Broadcasts Panel
  215. wrapper.find('Broadcasts SidebarItem').simulate('click');
  216. await tick();
  217. wrapper.update();
  218. expect(wrapper.find('SidebarPanel')).toHaveLength(0);
  219. // Close the sidebar
  220. wrapper.find('Broadcasts SidebarItem').simulate('click');
  221. });
  222. it('can unmount Sidebar (and Broadcasts) and kills Broadcast timers', async function () {
  223. jest.useFakeTimers();
  224. wrapper = createWrapper();
  225. const broadcasts = wrapper.find('Broadcasts').instance();
  226. // This will start timer to mark as seen
  227. await wrapper.find('Broadcasts SidebarItem').simulate('click');
  228. wrapper.update();
  229. jest.advanceTimersByTime(500);
  230. expect(broadcasts.poller).toBeDefined();
  231. expect(broadcasts.timer).toBeDefined();
  232. // Unmounting will cancel timers
  233. wrapper.unmount();
  234. expect(broadcasts.poller).toBe(null);
  235. expect(broadcasts.timer).toBe(null);
  236. // This advances timers enough so that mark as seen should be called if it wasn't unmounted
  237. jest.advanceTimersByTime(600);
  238. expect(apiMocks.broadcastsMarkAsSeen).not.toHaveBeenCalled();
  239. jest.useRealTimers();
  240. });
  241. it('can show Incidents in Sidebar Panel', async function () {
  242. incidentActions.loadIncidents = jest.fn(() => ({
  243. incidents: [TestStubs.ServiceIncident()],
  244. }));
  245. wrapper = createWrapper();
  246. await tick();
  247. wrapper.find('ServiceIncidents').simulate('click');
  248. await tick();
  249. wrapper.update();
  250. expect(wrapper.find('SidebarPanel')).toHaveLength(1);
  251. expect(wrapper.find('IncidentList')).toSnapshot();
  252. });
  253. it('hides when path changes', async function () {
  254. wrapper = createWrapper();
  255. wrapper.update();
  256. wrapper.find('Broadcasts SidebarItem').simulate('click');
  257. await tick();
  258. wrapper.update();
  259. expect(wrapper.find('SidebarPanel')).toHaveLength(1);
  260. const prevProps = wrapper.props();
  261. wrapper.setProps({
  262. location: {...router.location, pathname: 'new-path-name'},
  263. });
  264. // XXX(epurkhsier): Due to a bug in enzyme [0], componentDidUpdate is not
  265. // called after props have updated, it still receives _old_ `this.props`.
  266. // We manually call it here after the props have been correctly updated.
  267. //
  268. // [0]: https://github.com/enzymejs/enzyme/issues/2197
  269. wrapper.find('Sidebar').instance().componentDidUpdate(prevProps);
  270. await tick();
  271. wrapper.update();
  272. expect(wrapper.find('SidebarPanel')).toHaveLength(0);
  273. });
  274. });
  275. });