feat(core-component): capture sidebar item clicks

Signed-off-by: Camila Belo <camilaibs@gmail.com>
This commit is contained in:
Camila Belo
2023-04-25 13:21:52 +02:00
parent eecf5f100f
commit acadae7dde
2 changed files with 109 additions and 14 deletions
@@ -15,7 +15,11 @@
*/
import React from 'react';
import { renderInTestApp } from '@backstage/test-utils';
import {
MockAnalyticsApi,
TestApiProvider,
renderInTestApp,
} from '@backstage/test-utils';
import { createEvent, fireEvent, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import HomeIcon from '@material-ui/icons/Home';
@@ -24,6 +28,7 @@ import { Sidebar } from './Bar';
import { SidebarItem, SidebarSearchField, SidebarExpandButton } from './Items';
import { renderHook } from '@testing-library/react-hooks';
import { makeStyles } from '@material-ui/core/styles';
import { analyticsApiRef } from '@backstage/core-plugin-api';
const useStyles = makeStyles({
spotlight: {
@@ -31,27 +36,50 @@ const useStyles = makeStyles({
},
});
let analyticsApiMock: MockAnalyticsApi;
const handleSidebarItemClick = jest.fn();
async function renderSidebar() {
const { result } = renderHook(() => useStyles());
await renderInTestApp(
<Sidebar>
<SidebarSearchField onSearch={() => {}} to="/search" />
<SidebarItem text="Home" icon={HomeIcon} to="./" />
<SidebarItem
icon={CreateComponentIcon}
onClick={() => {}}
text="Create..."
className={result.current.spotlight}
/>
<SidebarExpandButton />
</Sidebar>,
<TestApiProvider apis={[[analyticsApiRef, analyticsApiMock]]}>
<Sidebar>
<SidebarSearchField onSearch={() => {}} to="/search" />
<SidebarItem text="Home" icon={HomeIcon} to="./" />
<SidebarItem
icon={CreateComponentIcon}
onClick={handleSidebarItemClick}
text="Create..."
className={result.current.spotlight}
/>
<SidebarItem
icon={CreateComponentIcon}
to="/docs"
onClick={handleSidebarItemClick}
text="Docs"
className={result.current.spotlight}
/>
<SidebarItem
icon={CreateComponentIcon}
to="/explore"
onClick={handleSidebarItemClick}
text="Explore"
className={result.current.spotlight}
noTrack
/>
<SidebarExpandButton />
</Sidebar>
</TestApiProvider>,
);
await userEvent.hover(screen.getByTestId('sidebar-root'));
}
describe('Items', () => {
beforeEach(async () => {
jest.clearAllMocks();
analyticsApiMock = new MockAnalyticsApi();
await renderSidebar();
});
@@ -73,6 +101,40 @@ describe('Items', () => {
await screen.findByRole('button', { name: /create/i }),
).toHaveStyle(`background-color: transparent`);
});
it('should send button clicks to analytics', async () => {
await userEvent.click(
await screen.findByRole('button', { name: /create/i }),
);
expect(handleSidebarItemClick).toHaveBeenCalledTimes(1);
expect(analyticsApiMock.getEvents()).toHaveLength(1);
expect(analyticsApiMock.getEvents()[0]).toMatchObject({
action: 'click',
subject: 'Create...',
context: { routeRef: 'unknown', pluginId: 'root', extension: 'App' },
attributes: { to: '/' },
});
});
it('should send link clicks to analytics', async () => {
await userEvent.click(await screen.findByRole('link', { name: /docs/i }));
expect(handleSidebarItemClick).toHaveBeenCalledTimes(1);
expect(analyticsApiMock.getEvents()).toHaveLength(1);
expect(analyticsApiMock.getEvents()[0]).toMatchObject({
action: 'click',
subject: 'Docs',
context: { routeRef: 'unknown', pluginId: 'root', extension: 'App' },
attributes: { to: '/docs' },
});
});
it('should not send clicks to analytics when tracking is disabled', async () => {
await userEvent.click(
await screen.findByRole('link', { name: /explore/i }),
);
expect(handleSidebarItemClick).toHaveBeenCalledTimes(1);
expect(analyticsApiMock.getEvents()).toHaveLength(0);
});
});
describe('SidebarSearchField', () => {
it('should be defaultPrevented when enter is pressed', async () => {
@@ -13,7 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { IconComponent, useElementFilter } from '@backstage/core-plugin-api';
import {
IconComponent,
useAnalytics,
useElementFilter,
} from '@backstage/core-plugin-api';
import { BackstageTheme } from '@backstage/theme';
import Badge from '@material-ui/core/Badge';
import Box from '@material-ui/core/Box';
@@ -38,6 +42,7 @@ import React, {
forwardRef,
KeyboardEventHandler,
ReactNode,
useCallback,
useContext,
useMemo,
useState,
@@ -264,6 +269,8 @@ type SidebarItemBaseProps = {
hasSubmenu?: boolean;
disableHighlight?: boolean;
className?: string;
noTrack?: boolean;
onClick?: (ev: React.MouseEvent) => void;
};
type SidebarItemButtonProps = SidebarItemBaseProps & {
@@ -362,6 +369,7 @@ const SidebarItemBase = forwardRef<any, SidebarItemProps>((props, ref) => {
hasSubmenu = false,
disableHighlight = false,
onClick,
noTrack,
children,
className,
...navLinkProps
@@ -424,9 +432,33 @@ const SidebarItemBase = forwardRef<any, SidebarItemProps>((props, ref) => {
),
};
const analyticsApi = useAnalytics();
const { pathname: to } = useResolvedPath(
!isButtonItem(props) && props.to ? props.to : '',
);
const handleClick = useCallback(
(event: React.MouseEvent<HTMLAnchorElement | HTMLButtonElement>) => {
if (!noTrack) {
const action = 'click';
const subject = text ?? 'Sidebar Item';
const options = to ? { attributes: { to } } : undefined;
analyticsApi.captureEvent(action, subject, options);
}
onClick?.(event);
},
[analyticsApi, text, to, noTrack, onClick],
);
if (isButtonItem(props)) {
return (
<Button role="button" aria-label={text} {...childProps} ref={ref}>
<Button
role="button"
aria-label={text}
{...childProps}
ref={ref}
onClick={handleClick}
>
{content}
</Button>
);
@@ -440,6 +472,7 @@ const SidebarItemBase = forwardRef<any, SidebarItemProps>((props, ref) => {
ref={ref}
aria-label={text ? text : props.to}
{...navLinkProps}
onClick={handleClick}
>
{content}
</WorkaroundNavLink>