123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426 |
- import type {UseQueryResult} from '@tanstack/react-query';
- import {BroadcastFixture} from 'sentry-fixture/broadcast';
- import {LocationFixture} from 'sentry-fixture/locationFixture';
- import {OrganizationFixture} from 'sentry-fixture/organization';
- import {ServiceIncidentFixture} from 'sentry-fixture/serviceIncident';
- import {UserFixture} from 'sentry-fixture/user';
- import {act, render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary';
- import {logout} from 'sentry/actionCreators/account';
- import {OnboardingContextProvider} from 'sentry/components/onboarding/onboardingContext';
- import SidebarContainer from 'sentry/components/sidebar';
- import ConfigStore from 'sentry/stores/configStore';
- import type {Organization} from 'sentry/types/organization';
- import type {StatuspageIncident} from 'sentry/types/system';
- import localStorage from 'sentry/utils/localStorage';
- import {useLocation} from 'sentry/utils/useLocation';
- import * as incidentsHook from 'sentry/utils/useServiceIncidents';
- jest.mock('sentry/actionCreators/account');
- jest.mock('sentry/utils/useServiceIncidents');
- jest.mock('sentry/utils/useLocation');
- const mockUseLocation = jest.mocked(useLocation);
- const ALL_AVAILABLE_FEATURES = [
- 'insights-entry-points',
- 'discover',
- 'discover-basic',
- 'discover-query',
- 'dashboards-basic',
- 'dashboards-edit',
- 'custom-metrics',
- 'user-feedback-ui',
- 'session-replay-ui',
- 'performance-view',
- 'performance-trace-explorer',
- 'profiling',
- ];
- describe('Sidebar', function () {
- const organization = OrganizationFixture();
- const broadcast = BroadcastFixture();
- const user = UserFixture();
- const apiMocks = {
- broadcasts: jest.fn(),
- broadcastsMarkAsSeen: jest.fn(),
- sdkUpdates: jest.fn(),
- };
- const getElement = () => (
- <OnboardingContextProvider>
- <SidebarContainer />
- </OnboardingContextProvider>
- );
- const renderSidebar = ({organization: org}: {organization: Organization | null}) =>
- render(getElement(), {organization: org});
- const renderSidebarWithFeatures = (features: string[] = []) => {
- return renderSidebar({
- organization: {
- ...organization,
- features: [...organization.features, ...features],
- },
- });
- };
- beforeEach(function () {
- mockUseLocation.mockReturnValue(LocationFixture());
- jest.spyOn(incidentsHook, 'useServiceIncidents').mockImplementation(
- () =>
- ({
- data: [ServiceIncidentFixture()],
- }) as UseQueryResult<StatuspageIncident[]>
- );
- apiMocks.broadcasts = MockApiClient.addMockResponse({
- url: `/organizations/${organization.slug}/broadcasts/`,
- body: [broadcast],
- });
- apiMocks.broadcastsMarkAsSeen = MockApiClient.addMockResponse({
- url: '/broadcasts/',
- method: 'PUT',
- });
- apiMocks.sdkUpdates = MockApiClient.addMockResponse({
- url: `/organizations/${organization.slug}/sdk-updates/`,
- body: [],
- });
- MockApiClient.addMockResponse({
- url: `/organizations/${organization.slug}/onboarding-tasks/`,
- method: 'GET',
- body: {
- onboardingTasks: [],
- },
- });
- });
- afterEach(function () {
- mockUseLocation.mockReset();
- });
- it('renders', async function () {
- renderSidebar({organization});
- expect(await screen.findByTestId('sidebar-dropdown')).toBeInTheDocument();
- });
- it('renders without org', async function () {
- renderSidebar({organization: null});
- // no org displays user details
- expect(await screen.findByText(user.name)).toBeInTheDocument();
- expect(screen.getByText(user.email)).toBeInTheDocument();
- await userEvent.click(screen.getByTestId('sidebar-dropdown'));
- });
- it('does not render collapse with navigation-sidebar-v2 flag', async function () {
- renderSidebar({
- organization: {...organization, features: ['navigation-sidebar-v2']},
- });
- // await for the page to be rendered
- expect(await screen.findByText('Issues')).toBeInTheDocument();
- // Check that the user name is no longer visible
- expect(screen.queryByText(user.name)).not.toBeInTheDocument();
- // Check that the organization name is no longer visible
- expect(screen.queryByText(organization.name)).not.toBeInTheDocument();
- expect(screen.queryByTestId('sidebar-collapse')).not.toBeInTheDocument();
- });
- it('has can logout', async function () {
- renderSidebar({
- organization: OrganizationFixture({access: ['member:read']}),
- });
- await userEvent.click(await screen.findByTestId('sidebar-dropdown'));
- await userEvent.click(screen.getByTestId('sidebar-signout'));
- await waitFor(() => expect(logout).toHaveBeenCalled());
- });
- it('can toggle help menu', async function () {
- renderSidebar({organization});
- await userEvent.click(await screen.findByText('Help'));
- expect(screen.getByText('Visit Help Center')).toBeInTheDocument();
- });
- describe('SidebarDropdown', function () {
- it('can open Sidebar org/name dropdown menu', async function () {
- renderSidebar({organization});
- await userEvent.click(await screen.findByTestId('sidebar-dropdown'));
- const orgSettingsLink = screen.getByText('Organization settings');
- expect(orgSettingsLink).toBeInTheDocument();
- });
- it('has link to Members settings with `member:write`', async function () {
- renderSidebar({
- organization: OrganizationFixture({access: ['member:read']}),
- });
- await userEvent.click(await screen.findByTestId('sidebar-dropdown'));
- expect(screen.getByText('Members')).toBeInTheDocument();
- });
- it('can open "Switch Organization" sub-menu', async function () {
- act(() => void ConfigStore.set('features', new Set(['organizations:create'])));
- renderSidebar({organization});
- await userEvent.click(await screen.findByTestId('sidebar-dropdown'));
- jest.useFakeTimers();
- await userEvent.hover(screen.getByText('Switch organization'), {delay: null});
- act(() => jest.advanceTimersByTime(500));
- jest.useRealTimers();
- const createOrg = screen.getByText('Create a new organization');
- expect(createOrg).toBeInTheDocument();
- });
- });
- describe('SidebarPanel', function () {
- it('hides when path changes', async function () {
- const {rerender} = renderSidebar({organization});
- await userEvent.click(await screen.findByText("What's new"));
- expect(await screen.findByRole('dialog')).toBeInTheDocument();
- expect(screen.getByText("What's new in Sentry")).toBeInTheDocument();
- mockUseLocation.mockReturnValue({...LocationFixture(), pathname: '/other/path'});
- rerender(getElement());
- expect(screen.queryByText("What's new in Sentry")).not.toBeInTheDocument();
- });
- it('can have onboarding feature', async function () {
- renderSidebar({
- organization: {...organization, features: ['onboarding']},
- });
- const quickStart = await screen.findByText('Onboarding');
- expect(quickStart).toBeInTheDocument();
- await userEvent.click(quickStart);
- expect(await screen.findByRole('dialog')).toBeInTheDocument();
- expect(screen.getByText('Capture your first error')).toBeInTheDocument();
- await userEvent.click(quickStart);
- expect(screen.queryByText('Capture your first error')).not.toBeInTheDocument();
- await tick();
- });
- it('displays empty panel when there are no Broadcasts', async function () {
- MockApiClient.addMockResponse({
- url: `/organizations/${organization.slug}/broadcasts/`,
- body: [],
- });
- renderSidebar({organization});
- await userEvent.click(await screen.findByText("What's new"));
- expect(await screen.findByRole('dialog')).toBeInTheDocument();
- expect(screen.getByText("What's new in Sentry")).toBeInTheDocument();
- expect(
- screen.getByText('No recent updates from the Sentry team.')
- ).toBeInTheDocument();
- // Close the sidebar
- await userEvent.click(screen.getByText("What's new"));
- expect(screen.queryByText("What's new in Sentry")).not.toBeInTheDocument();
- await tick();
- });
- it('can display Broadcasts panel and mark as seen', async function () {
- jest.useFakeTimers();
- renderSidebar({organization});
- expect(apiMocks.broadcasts).toHaveBeenCalled();
- await userEvent.click(await screen.findByText("What's new"), {delay: null});
- expect(await screen.findByRole('dialog')).toBeInTheDocument();
- expect(screen.getByText("What's new in Sentry")).toBeInTheDocument();
- const broadcastTitle = screen.getByText(broadcast.title);
- expect(broadcastTitle).toBeInTheDocument();
- // Should mark as seen after a delay
- act(() => jest.advanceTimersByTime(2000));
- await waitFor(() => {
- expect(apiMocks.broadcastsMarkAsSeen).toHaveBeenCalledWith(
- '/broadcasts/',
- expect.objectContaining({
- data: {hasSeen: '1'},
- query: {id: ['8']},
- })
- );
- });
- jest.useRealTimers();
- // Close the sidebar
- await userEvent.click(screen.getByText("What's new"));
- expect(screen.queryByText("What's new in Sentry")).not.toBeInTheDocument();
- await tick();
- });
- it('can unmount Sidebar (and Broadcasts) and kills Broadcast timers', async function () {
- jest.useFakeTimers();
- const {unmount} = renderSidebar({organization});
- // This will start timer to mark as seen
- await userEvent.click(await screen.findByTestId('sidebar-broadcasts'), {
- delay: null,
- });
- expect(await screen.findByText("What's new in Sentry")).toBeInTheDocument();
- act(() => jest.advanceTimersByTime(500));
- // Unmounting will cancel timers
- unmount();
- // This advances timers enough so that mark as seen should be called if
- // it wasn't unmounted
- act(() => jest.advanceTimersByTime(600));
- expect(apiMocks.broadcastsMarkAsSeen).not.toHaveBeenCalled();
- jest.useRealTimers();
- });
- it('can show Incidents in Sidebar Panel', async function () {
- renderSidebar({organization});
- await userEvent.click(await screen.findByText(/Service status/));
- await screen.findByText('Recent service updates');
- });
- });
- it('can toggle collapsed state', async function () {
- renderSidebar({organization});
- expect(await screen.findByText(user.name)).toBeInTheDocument();
- expect(screen.getByText(organization.name)).toBeInTheDocument();
- await userEvent.click(screen.getByTestId('sidebar-collapse'));
- // Check that the organization name is no longer visible
- expect(screen.queryByText(organization.name)).not.toBeInTheDocument();
- // Un-collapse he sidebar and make sure the org name is visible again
- await userEvent.click(screen.getByTestId('sidebar-collapse'));
- expect(await screen.findByText(organization.name)).toBeInTheDocument();
- });
- describe('sidebar links', () => {
- beforeEach(function () {
- ConfigStore.init();
- ConfigStore.set('features', new Set([]));
- ConfigStore.set('user', user);
- mockUseLocation.mockReturnValue({...LocationFixture()});
- });
- it('renders navigation', async function () {
- renderSidebar({organization});
- await waitFor(function () {
- expect(apiMocks.broadcasts).toHaveBeenCalled();
- });
- expect(
- screen.getByRole('navigation', {name: 'Primary Navigation'})
- ).toBeInTheDocument();
- });
- it('in self-hosted-errors-only mode, only shows links to basic features', async function () {
- ConfigStore.set('isSelfHostedErrorsOnly', true);
- renderSidebarWithFeatures(ALL_AVAILABLE_FEATURES);
- await waitFor(function () {
- expect(apiMocks.broadcasts).toHaveBeenCalled();
- });
- const links = screen.getAllByRole('link');
- expect(links).toHaveLength(12);
- [
- 'Issues',
- 'Projects',
- 'Alerts',
- 'Discover',
- 'Dashboards',
- 'Releases',
- 'Stats',
- 'Settings',
- 'Help',
- /What's new/,
- /Service status/,
- ].forEach((title, index) => {
- expect(links[index]).toHaveAccessibleName(title);
- });
- });
- it('in regular mode, also shows links to Performance and Crons', async function () {
- localStorage.setItem('sidebar-accordion-insights:expanded', 'true');
- renderSidebarWithFeatures([...ALL_AVAILABLE_FEATURES]);
- await waitFor(function () {
- expect(apiMocks.broadcasts).toHaveBeenCalled();
- });
- const links = screen.getAllByRole('link');
- expect(links).toHaveLength(25);
- [
- 'Issues',
- 'Projects',
- /Explore/,
- /Traces/,
- /Metrics/,
- 'Profiles',
- 'Replays',
- 'Discover',
- /Insights/,
- 'Frontend',
- 'Backend',
- 'Mobile',
- 'AI',
- 'Performance',
- 'User Feedback',
- 'Crons',
- 'Alerts',
- 'Dashboards',
- 'Releases',
- 'Stats',
- 'Settings',
- 'Help',
- /What's new/,
- /Service status/,
- ].forEach((title, index) => {
- expect(links[index]).toHaveAccessibleName(title);
- });
- });
- it('should not render floating accordion when expanded', async () => {
- renderSidebarWithFeatures(ALL_AVAILABLE_FEATURES);
- await userEvent.click(
- screen.getByTestId('sidebar-accordion-insights-domains-item')
- );
- expect(screen.queryByTestId('floating-accordion')).not.toBeInTheDocument();
- });
- it('should render floating accordion when collapsed', async () => {
- renderSidebarWithFeatures(ALL_AVAILABLE_FEATURES);
- await userEvent.click(screen.getByTestId('sidebar-collapse'));
- await userEvent.click(
- screen.getByTestId('sidebar-accordion-insights-domains-item')
- );
- expect(await screen.findByTestId('floating-accordion')).toBeInTheDocument();
- });
- });
- });
|