Test: Add unit tests for the homepage (#95912)

* test: add skeleton for initial unit tests

* test: add basic tests for DataTrailsHome

* test-wip: add tests for recent metrics functionality, but need to move from DataTrailsHome.test to DataTrailsRecentMetrics.test

* wip: tests for DataTrailBookmarks

* test: add tests for recent metrics; refactor: make DataTrailsRecentMetrics accept onSelect as prop rather than whole trail; test: add tests for DataTrailCard (WIP)

* test: add test for truncates long list of labels after 3 lines in recent explorations

* refactor: make DataTrailBookmarks take in onSelect and onDelete as props rather than a whole trail and onDelete; test: add tests for bookmarks (WIP)

* remove deprecated style

* fix import issues

* fix getTrailForBookmark tests by returning a trail, clean up tests

* chore: delete notes to self

---------

Co-authored-by: Brendan O'Handley <brendan.ohandley@grafana.com>
pull/96862/head
Kat Yang 7 months ago committed by GitHub
parent 30b3fd2864
commit c8bc1f8637
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 68
      public/app/features/trails/DataTrailBookmarks.test.tsx
  2. 13
      public/app/features/trails/DataTrailBookmarks.tsx
  3. 73
      public/app/features/trails/DataTrailCard.test.tsx
  4. 1
      public/app/features/trails/DataTrailCard.tsx
  5. 73
      public/app/features/trails/DataTrailsHome.test.tsx
  6. 4
      public/app/features/trails/DataTrailsHome.tsx
  7. 115
      public/app/features/trails/DataTrailsRecentMetrics.test.tsx
  8. 9
      public/app/features/trails/DataTrailsRecentMetrics.tsx

@ -0,0 +1,68 @@
import { render, screen, fireEvent } from '@testing-library/react';
import { DataTrail } from './DataTrail';
import { DataTrailsBookmarks } from './DataTrailBookmarks';
import { getTrailStore, DataTrailBookmark } from './TrailStore/TrailStore';
jest.mock('./TrailStore/TrailStore', () => ({
getTrailStore: jest.fn(),
getBookmarkKey: jest.fn(() => 'bookmark-key'),
}));
const onSelect = jest.fn();
const onDelete = jest.fn();
describe('DataTrailsBookmarks', () => {
const trail = new DataTrail({});
const bookmark: DataTrailBookmark = { urlValues: { key: '1', metric: '' }, createdAt: Date.now() };
beforeEach(() => {
onSelect.mockClear();
(getTrailStore as jest.Mock).mockImplementation(() => ({
bookmarks: [],
recent: [],
getTrailForBookmark: jest.fn(),
}));
});
it('does not render if there are no bookmarks', () => {
render(<DataTrailsBookmarks onSelect={onSelect} onDelete={onDelete} />);
expect(screen.queryByText('Or view bookmarks')).not.toBeInTheDocument();
});
it('renders the bookmarks header and toggle button', () => {
(getTrailStore as jest.Mock).mockImplementation(() => ({
bookmarks: [bookmark],
recent: [],
}));
render(<DataTrailsBookmarks onSelect={onSelect} onDelete={onDelete} />);
expect(screen.getByText('Or view bookmarks')).toBeInTheDocument();
expect(screen.getByLabelText('bookmarkCarrot')).toBeInTheDocument();
});
it('toggles the bookmark list when the toggle button is clicked', () => {
(getTrailStore as jest.Mock).mockImplementation(() => ({
bookmarks: [bookmark],
recent: [],
getTrailForBookmark: jest.fn().mockReturnValue(trail),
}));
render(<DataTrailsBookmarks onSelect={onSelect} onDelete={onDelete} />);
const button = screen.getByLabelText('bookmarkCarrot');
fireEvent.click(button);
expect(screen.getByText('Select metric')).toBeInTheDocument();
fireEvent.click(button);
expect(screen.queryByText('Select metric')).not.toBeInTheDocument();
});
it('calls onDelete when the delete button is clicked', () => {
(getTrailStore as jest.Mock).mockImplementation(() => ({
bookmarks: [bookmark],
recent: [],
getTrailForBookmark: jest.fn().mockReturnValue(trail),
}));
render(<DataTrailsBookmarks onSelect={onSelect} onDelete={onDelete} />);
fireEvent.click(screen.getByLabelText('bookmarkCarrot'));
fireEvent.click(screen.getByLabelText('Remove bookmark'));
expect(onDelete).toHaveBeenCalled();
});
});

@ -2,19 +2,18 @@ import { css } from '@emotion/css';
import { useState } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { SceneComponentProps } from '@grafana/scenes';
import { IconButton, useStyles2 } from '@grafana/ui';
import { Trans } from 'app/core/internationalization';
import { DataTrailCard } from './DataTrailCard';
import { DataTrailsHome } from './DataTrailsHome';
import { getTrailStore, getBookmarkKey } from './TrailStore/TrailStore';
interface Props extends SceneComponentProps<DataTrailsHome> {
type Props = {
onSelect: (index: number) => void;
onDelete: (index: number) => void;
}
};
export function DataTrailsBookmarks({ model, onDelete }: Props) {
export function DataTrailsBookmarks({ onSelect, onDelete }: Props) {
const [toggleBookmark, setToggleBookmark] = useState(false);
const styles = useStyles2(getStyles);
@ -31,7 +30,7 @@ export function DataTrailsBookmarks({ model, onDelete }: Props) {
</div>
<IconButton
name={toggleBookmark ? 'angle-up' : 'angle-down'}
size="xxxl"
size="xl"
aria-label="bookmarkCarrot"
variant="secondary"
onClick={() => setToggleBookmark(!toggleBookmark)}
@ -44,7 +43,7 @@ export function DataTrailsBookmarks({ model, onDelete }: Props) {
<DataTrailCard
key={getBookmarkKey(bookmark)}
bookmark={bookmark}
onSelect={() => model.onSelectBookmark(index)}
onSelect={() => onSelect(index)}
onDelete={() => onDelete(index)}
/>
);

@ -0,0 +1,73 @@
import { render, screen, fireEvent } from '@testing-library/react';
import { DataTrail } from './DataTrail';
import { DataTrailCard } from './DataTrailCard';
import { DataTrailBookmark } from './TrailStore/TrailStore';
jest.mock('./utils', () => ({
...jest.requireActual('./utils'),
getDataSource: jest.fn(() => 'Test DataSource'),
getDataSourceName: jest.fn(() => 'Test DataSource Name'),
}));
describe('DataTrailCard', () => {
// trail is a recent metric exploration
const trail = new DataTrail({ key: '1', metric: 'Test Recent Exploration' });
// bookmark is a data trail stored in a url
const bookmark: DataTrailBookmark = { urlValues: { key: '1', metric: 'Test Bookmark' }, createdAt: Date.now() };
const onSelect = jest.fn();
const onDelete = jest.fn();
beforeEach(() => {
onSelect.mockClear();
onDelete.mockClear();
});
it('renders the card with recent metric exploration', () => {
render(<DataTrailCard trail={trail} onSelect={onSelect} onDelete={onDelete} />);
expect(screen.getByText('Test Recent Exploration')).toBeInTheDocument();
});
it('renders the card with bookmark', () => {
render(<DataTrailCard bookmark={bookmark} onSelect={onSelect} onDelete={onDelete} />);
expect(screen.getByText('Test Bookmark')).toBeInTheDocument();
});
it('calls onSelect when the card is clicked', () => {
render(<DataTrailCard bookmark={bookmark} onSelect={onSelect} onDelete={onDelete} />);
fireEvent.click(screen.getByText('Test Bookmark'));
expect(onSelect).toHaveBeenCalled();
});
it('calls onDelete when the delete button is clicked', () => {
render(<DataTrailCard bookmark={bookmark} onSelect={onSelect} onDelete={onDelete} />);
fireEvent.click(screen.getByTestId('deleteButton'));
expect(onDelete).toHaveBeenCalled();
});
it('truncates singular long label in recent explorations', () => {
const longLabel =
'aajalsdkfaldkjfalskdjfalsdkjfalsdkjflaskjdflaskjdflaskjdflaskjdflasjkdflaskjdflaskjdflaskjflaskdjfldaskjflasjflaskdjflaskjflasjflaskfjalsdfjlskdjflaskjdflajkfjfalkdfjaverylongalskdjlalsjflajkfklsajdfalskjdflkasjdflkadjf';
const bookmarkWithLongLabel: DataTrailBookmark = {
urlValues: { key: '1', metric: 'metric', 'var-filters': `zone|=|${longLabel}` },
createdAt: Date.now(),
};
render(<DataTrailCard bookmark={bookmarkWithLongLabel} onSelect={onSelect} onDelete={onDelete} />);
expect(screen.getByText('...', { exact: false })).toBeInTheDocument();
});
it('truncates long list of labels after 3 lines in recent explorations', () => {
const bookmarkWithLongLabel: DataTrailBookmark = {
urlValues: {
key: '1',
metric: 'metric',
// labels are in a comma separated list
'var-filters': `zone|=|averylonglabeltotakeupspace,zone=averylonglabeltotakeupspace,zone1=averylonglabeltotakeupspace,zone2=averylonglabeltotakeupspace,zone3=averylonglabeltotakeupspace,zone4=averylonglabeltotakeupspace`,
},
createdAt: Date.now(),
};
render(<DataTrailCard bookmark={bookmarkWithLongLabel} onSelect={onSelect} onDelete={onDelete} />);
// to test the non-existence of a truncated label we need queryByText
const truncatedLabel = screen.queryByText('zone4');
expect(truncatedLabel).not.toBeInTheDocument();
});
});

@ -91,6 +91,7 @@ export function DataTrailCard(props: Props) {
className={styles.secondary}
tooltip="Remove bookmark"
onClick={onDelete}
data-testid="deleteButton"
/>
</Card.SecondaryActions>
)}

@ -0,0 +1,73 @@
import { render, screen } from '@testing-library/react';
import { AdHocFiltersVariable, sceneGraph, SceneObjectRef, SceneVariableSet } from '@grafana/scenes';
import { DataTrail } from './DataTrail';
import { DataTrailsHome } from './DataTrailsHome';
import { getTrailStore } from './TrailStore/TrailStore';
import { VAR_FILTERS } from './shared';
jest.mock('./TrailStore/TrailStore', () => ({
getTrailStore: jest.fn(),
}));
describe('DataTrailsHome', () => {
let scene: DataTrailsHome;
beforeEach(() => {
const filtersVariable = new AdHocFiltersVariable({ name: VAR_FILTERS });
(getTrailStore as jest.Mock).mockImplementation(() => ({
bookmarks: [],
recent: [],
}));
scene = new DataTrailsHome({
$variables: new SceneVariableSet({
variables: [filtersVariable],
}),
});
});
it('renders the start button', () => {
render(<scene.Component model={scene} />);
expect(screen.getByText("Let's start!")).toBeInTheDocument();
});
it('renders the learn more button and checks its href', () => {
render(<scene.Component model={scene} />);
const learnMoreButton = screen.getByText('Learn more');
expect(learnMoreButton).toBeInTheDocument();
expect(learnMoreButton.closest('a')).toHaveAttribute(
'href',
'https://grafana.com/docs/grafana/latest/explore/explore-metrics/'
);
});
it('does not show recent metrics and bookmarks headers for first time user', () => {
render(<scene.Component model={scene} />);
expect(screen.queryByText('Or view a recent exploration')).not.toBeInTheDocument();
expect(screen.queryByText('Or view bookmarks')).not.toBeInTheDocument();
expect(screen.queryByRole('separator')).not.toBeInTheDocument();
});
it('truncates singular long label in recent explorations', () => {
const trail = new DataTrail({});
function getFilterVar() {
const variable = sceneGraph.lookupVariable(VAR_FILTERS, trail);
if (variable instanceof AdHocFiltersVariable) {
return variable;
}
throw new Error('getFilterVar failed');
}
const filtersVariable = getFilterVar();
const longLabel = 'averylongalskdjlalsjflajkfklsajdfalskjdflkasjdflkadjf';
filtersVariable.setState({
filters: [{ key: 'zone', operator: '=', value: longLabel }],
});
const trailWithResolveMethod = new SceneObjectRef(trail);
(getTrailStore as jest.Mock).mockImplementation(() => ({
bookmarks: [],
recent: [trailWithResolveMethod],
}));
render(<scene.Component model={scene} />);
expect(screen.getByText('...', { exact: false })).toBeInTheDocument();
});
});

@ -88,8 +88,8 @@ export class DataTrailsHome extends SceneObjectBase<DataTrailsHomeState> {
</div>
</Stack>
</div>
<DataTrailsRecentMetrics model={model} />
<DataTrailsBookmarks model={model} onDelete={onDelete} />
<DataTrailsRecentMetrics onSelect={model.onSelectRecentTrail} />
<DataTrailsBookmarks onSelect={model.onSelectBookmark} onDelete={onDelete} />
</div>
);
};

@ -0,0 +1,115 @@
import { render, screen, fireEvent } from '@testing-library/react';
import { SceneObjectRef } from '@grafana/scenes';
import { DataTrail } from './DataTrail';
import { DataTrailsRecentMetrics } from './DataTrailsRecentMetrics';
import { getTrailStore } from './TrailStore/TrailStore';
jest.mock('./TrailStore/TrailStore', () => ({
getTrailStore: jest.fn(),
}));
const onSelect = jest.fn();
describe('DataTrailsRecentMetrics', () => {
beforeEach(() => {
onSelect.mockClear();
(getTrailStore as jest.Mock).mockImplementation(() => ({
bookmarks: [],
recent: [],
}));
});
it('renders the recent metrics header if there is at least one recent metric', () => {
(getTrailStore as jest.Mock).mockImplementation(() => ({
bookmarks: [],
recent: [
{
resolve: () => ({ state: { key: '1' } }),
},
],
}));
render(<DataTrailsRecentMetrics onSelect={onSelect} />);
expect(screen.getByText('Or view a recent exploration')).toBeInTheDocument();
});
it('does not show the "Show more" button if there are 3 or fewer recent metrics', () => {
(getTrailStore as jest.Mock).mockImplementation(() => ({
bookmarks: [],
recent: [
{
resolve: () => ({ state: { key: '1' } }),
},
{
resolve: () => ({ state: { key: '2' } }),
},
{
resolve: () => ({ state: { key: '3' } }),
},
],
}));
render(<DataTrailsRecentMetrics onSelect={onSelect} />);
expect(screen.queryByText('Show more')).not.toBeInTheDocument();
});
it('shows the "Show more" button if there are more than 3 recent metrics', () => {
(getTrailStore as jest.Mock).mockImplementation(() => ({
bookmarks: [],
recent: [
{
resolve: () => ({ state: { key: '1' } }),
},
{
resolve: () => ({ state: { key: '2' } }),
},
{
resolve: () => ({ state: { key: '3' } }),
},
{
resolve: () => ({ state: { key: '4' } }),
},
],
}));
render(<DataTrailsRecentMetrics onSelect={onSelect} />);
expect(screen.getByText('Show more')).toBeInTheDocument();
});
it('toggles between "Show more" and "Show less" when the button is clicked', () => {
(getTrailStore as jest.Mock).mockImplementation(() => ({
bookmarks: [],
recent: [
{
resolve: () => ({ state: { key: '1' } }),
},
{
resolve: () => ({ state: { key: '2' } }),
},
{
resolve: () => ({ state: { key: '3' } }),
},
{
resolve: () => ({ state: { key: '4' } }),
},
],
}));
render(<DataTrailsRecentMetrics onSelect={onSelect} />);
const button = screen.getByText('Show more');
fireEvent.click(button);
expect(screen.getByText('Show less')).toBeInTheDocument();
fireEvent.click(button);
expect(screen.getByText('Show more')).toBeInTheDocument();
});
it('selecting a recent exploration card takes you to the metric', () => {
const trail = new DataTrail({ key: '1', metric: 'select me' });
const trailWithResolveMethod = new SceneObjectRef(trail);
(getTrailStore as jest.Mock).mockImplementation(() => ({
bookmarks: [],
recent: [trailWithResolveMethod],
}));
render(<DataTrailsRecentMetrics onSelect={onSelect} />);
fireEvent.click(screen.getByText('select me'));
expect(onSelect).toHaveBeenCalledWith(trail);
});
});

@ -2,15 +2,16 @@ import { css } from '@emotion/css';
import { useState } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { SceneComponentProps } from '@grafana/scenes';
import { Button, useStyles2, useTheme2 } from '@grafana/ui';
import { Trans } from 'app/core/internationalization';
import { DataTrail } from './DataTrail';
import { DataTrailCard } from './DataTrailCard';
import { DataTrailsHome } from './DataTrailsHome';
import { getTrailStore } from './TrailStore/TrailStore';
export function DataTrailsRecentMetrics({ model }: SceneComponentProps<DataTrailsHome>) {
type Props = { onSelect: (trail: DataTrail) => void };
export function DataTrailsRecentMetrics({ onSelect }: Props) {
const styles = useStyles2(getStyles);
const recentMetrics = getTrailStore().recent;
const theme = useTheme2();
@ -40,7 +41,7 @@ export function DataTrailsRecentMetrics({ model }: SceneComponentProps<DataTrail
<DataTrailCard
key={(resolvedTrail.state.key || '') + index}
trail={resolvedTrail}
onSelect={() => model.onSelectRecentTrail(resolvedTrail)}
onSelect={() => onSelect(resolvedTrail)}
/>
);
})}

Loading…
Cancel
Save