Merge pull request #18399 from Zaperex/lighthouse

feat(lighthouse): add more verbose components for "no audits found" case
This commit is contained in:
Ben Lambert
2023-07-04 15:28:38 +02:00
committed by GitHub
8 changed files with 190 additions and 25 deletions
+7
View File
@@ -0,0 +1,7 @@
---
'@backstage/plugin-lighthouse': patch
---
Added more verbose components (used to render `null`) when no audits for a website corresponding to the provided url were found.
Added `Create New Audit` button for the `AuditListForEntity` component used by `EntityLighthouseContent` and `EmbeddedRouter`.
Removed error alert from `errorApi` if error was due to no audits being found for a website (empty database query result).
+1
View File
@@ -409,6 +409,7 @@ unregistration
untracked
upsert
upvote
url
URIs
URLs
utils
@@ -26,16 +26,31 @@ import { lighthouseApiRef } from '../../api';
import { useWebsiteForEntity } from '../../hooks/useWebsiteForEntity';
import * as data from '../../__fixtures__/website-list-response.json';
import { AuditListForEntity } from './AuditListForEntity';
import { rootRouteRef } from '../../plugin';
import { fireEvent, screen } from '@testing-library/react';
jest.mock('../../hooks/useWebsiteForEntity', () => ({
useWebsiteForEntity: jest.fn(),
}));
jest.mock('react-router-dom', () => {
const actual = jest.requireActual('react-router-dom');
const mockNavigation = jest.fn();
return {
...actual,
useNavigate: jest.fn(() => mockNavigation),
};
});
const useWebsiteForEntityMock = useWebsiteForEntity as jest.Mock;
const websiteListResponse = data as WebsiteListResponse;
const entityWebsite = websiteListResponse.items[0];
const testAppOptions = {
mountedRoutes: { '/': rootRouteRef },
};
describe('<AuditListTableForEntity />', () => {
afterEach(() => {
jest.resetAllMocks();
@@ -73,39 +88,77 @@ describe('<AuditListTableForEntity />', () => {
loading: false,
error: null,
});
const { findByText } = await renderInTestApp(subject());
expect(await findByText(entityWebsite.url)).toBeInTheDocument();
const rendered = await renderInTestApp(subject(), testAppOptions);
const create_audit_button = await rendered.findByText('Create New Audit');
const support_button = await rendered.findByText('Support');
expect(await rendered.findByText(entityWebsite.url)).toBeInTheDocument();
expect(await rendered.findByText('Latest Audit')).toBeInTheDocument();
expect(create_audit_button).toBeInTheDocument();
expect(support_button).toBeInTheDocument();
});
it('renders a Progress element where the data is loading', async () => {
it('renders a Progress element when the data is loading', async () => {
useWebsiteForEntityMock.mockReturnValue({
value: null,
loading: true,
error: null,
});
const { findByTestId } = await renderInTestApp(subject());
const { findByTestId } = await renderInTestApp(subject(), testAppOptions);
expect(await findByTestId('progress')).toBeInTheDocument();
});
it('renders nothing where there is an error loading data', async () => {
it('renders a WarningPanel when there is an error loading data', async () => {
useWebsiteForEntityMock.mockReturnValue({
value: null,
loading: false,
error: 'error',
error: { name: 'error', message: 'error loading data' },
});
const { queryByTestId } = await renderInTestApp(subject());
expect(queryByTestId('AuditListTable')).toBeNull();
await renderInTestApp(subject(), testAppOptions);
const expandIcon = screen.getByText('Error: Could not load audit list.');
fireEvent.click(expandIcon);
expect(
screen.getByText('Error: Could not load audit list.'),
).toBeInTheDocument();
expect(screen.getByText('error loading data')).toBeInTheDocument();
});
it('renders nothing where there is not data', async () => {
it('renders an empty table when there is no data', async () => {
useWebsiteForEntityMock.mockReturnValue({
value: null,
loading: false,
error: null,
});
const { queryByTestId } = await renderInTestApp(subject());
expect(queryByTestId('AuditListTable')).toBeNull();
const rendered = await renderInTestApp(subject(), testAppOptions);
const create_audit_button = await rendered.findByText('Create New Audit');
const support_button = await rendered.findByText('Support');
expect(
await rendered.findByText('No records to display'),
).toBeInTheDocument();
expect(await rendered.findByText('Latest Audit')).toBeInTheDocument();
expect(create_audit_button).toBeInTheDocument();
expect(support_button).toBeInTheDocument();
});
it('renders an empty table when there is no data and error loading data due to empty database query result', async () => {
useWebsiteForEntityMock.mockReturnValue({
value: null,
loading: false,
error: {
name: 'error',
message: 'no audited website found for url unit-test-url',
},
});
const rendered = await renderInTestApp(subject(), testAppOptions);
const create_audit_button = await rendered.findByText('Create New Audit');
const support_button = await rendered.findByText('Support');
expect(
await rendered.findByText('No records to display'),
).toBeInTheDocument();
expect(await rendered.findByText('Latest Audit')).toBeInTheDocument();
expect(create_audit_button).toBeInTheDocument();
expect(support_button).toBeInTheDocument();
});
});
@@ -13,19 +13,65 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import React, { ReactNode } from 'react';
import { AuditListTable } from './AuditListTable';
import { useWebsiteForEntity } from '../../hooks/useWebsiteForEntity';
import { Progress } from '@backstage/core-components';
import { LIGHTHOUSE_WEBSITE_URL_ANNOTATION } from '../../../constants';
import {
Content,
ContentHeader,
InfoCard,
Progress,
WarningPanel,
} from '@backstage/core-components';
import { Button } from '@material-ui/core';
import { resolvePath, useNavigate } from 'react-router-dom';
import { useEntity } from '@backstage/plugin-catalog-react';
import { useRouteRef } from '@backstage/core-plugin-api';
import { rootRouteRef } from '../../plugin';
import LighthouseSupportButton from '../SupportButton';
export const AuditListForEntity = () => {
const { value, loading, error } = useWebsiteForEntity();
if (loading) {
return <Progress />;
}
if (error || !value) {
return null;
const { entity } = useEntity();
const navigate = useNavigate();
const fromPath = useRouteRef(rootRouteRef)?.() ?? '../';
let createAuditButtonUrl = 'create-audit';
const websiteUrl =
entity.metadata.annotations?.[LIGHTHOUSE_WEBSITE_URL_ANNOTATION] ?? '';
if (websiteUrl) {
createAuditButtonUrl += `?url=${encodeURIComponent(websiteUrl)}`;
}
return <AuditListTable data-test-id="AuditListTable" items={[value]} />;
let content: ReactNode = null;
content = (
<Content>
<ContentHeader title="Latest Audit">
<Button
variant="contained"
color="primary"
onClick={() => navigate(resolvePath(createAuditButtonUrl, fromPath))}
>
Create New Audit
</Button>
<LighthouseSupportButton />
</ContentHeader>
<AuditListTable items={value ? [value] : []} />
</Content>
);
if (loading) {
content = <Progress />;
}
if (error && !error.message.includes('no audited website found for url')) {
// We only want to display this warning panel when its caused by an error other than no audits for the website
content = (
<WarningPanel severity="error" title="Could not load audit list.">
{error.message}
</WarningPanel>
);
}
return <InfoCard noPadding>{content}</InfoCard>;
};
@@ -26,6 +26,7 @@ import {
import { useWebsiteForEntity } from '../../hooks/useWebsiteForEntity';
import * as data from '../../__fixtures__/website-list-response.json';
import { LastLighthouseAuditCard } from './LastLighthouseAuditCard';
import { fireEvent, screen } from '@testing-library/react';
jest.mock('../../hooks/useWebsiteForEntity', () => ({
useWebsiteForEntity: jest.fn(),
@@ -147,17 +148,22 @@ describe('<LastLighthouseAuditCard />', () => {
(useWebsiteForEntity as jest.Mock).mockReturnValue({
value: null,
loading: false,
error: 'error',
error: { name: 'error', message: 'error loading data' },
});
});
it('renders nothing', async () => {
const { queryByTestId } = await renderInTestApp(
it('renders a WarningPanel', async () => {
await renderInTestApp(
<EntityProvider entity={entity}>
<LastLighthouseAuditCard />
</EntityProvider>,
);
expect(queryByTestId('AuditListTable')).toBeNull();
const expandIcon = screen.getByText('Error: Could not load audit list.');
fireEvent.click(expandIcon);
expect(
screen.getByText('Error: Could not load audit list.'),
).toBeInTheDocument();
expect(screen.getByText('error loading data')).toBeInTheDocument();
});
});
@@ -179,4 +185,26 @@ describe('<LastLighthouseAuditCard />', () => {
expect(queryByTestId('AuditListTable')).toBeNull();
});
});
describe('where error was due to an empty database query result', () => {
beforeEach(() => {
(useWebsiteForEntity as jest.Mock).mockReturnValue({
value: null,
loading: false,
error: {
name: 'error',
message: 'no audited website found for url unit-test-url',
},
});
});
it('renders EmptyState card', async () => {
const rendered = await renderInTestApp(
<EntityProvider entity={entity}>
<LastLighthouseAuditCard />
</EntityProvider>,
);
expect(rendered.getByText('No Audits Found')).toBeInTheDocument();
});
});
});
@@ -23,6 +23,7 @@ import {
import { useWebsiteForEntity } from '../../hooks/useWebsiteForEntity';
import AuditStatusIcon from '../AuditStatusIcon';
import {
EmptyState,
InfoCard,
InfoCardVariants,
Progress,
@@ -30,6 +31,7 @@ import {
StatusOK,
StatusWarning,
StructuredMetadataTable,
WarningPanel,
} from '@backstage/core-components';
const LighthouseCategoryScoreStatus = (props: { score: number }) => {
@@ -105,7 +107,14 @@ export const LastLighthouseAuditCard = (props: {
content = <Progress />;
}
if (error) {
content = null;
// We only want to display this warning panel when its caused by an error other than no audits being found for the website
content = error.message.includes('no audited website found for url') ? (
<EmptyState title="No Audits Found" missing="data" />
) : (
<WarningPanel severity="error" title="Could not load audit list.">
{error.message}
</WarningPanel>
);
}
if (website) {
content = (
@@ -98,4 +98,21 @@ describe('useWebsiteForEntity', () => {
expect(mockErrorApi.post).toHaveBeenCalledWith(error);
});
});
describe('where there is an error regarding "no audited websites for url"', () => {
const error = new Error('no audited website found for url unit-test-url');
beforeEach(() => {
(mockLighthouseApi.getWebsiteByUrl as jest.Mock).mockRejectedValueOnce(
error,
);
});
it('does not post the error to the error api and returns the error to the caller', async () => {
const { result, waitForNextUpdate } = subject();
await waitForNextUpdate();
expect(result.current?.error).toBe(error);
expect(mockErrorApi.post).not.toHaveBeenCalledWith(error);
});
});
});
@@ -31,7 +31,11 @@ export const useWebsiteForEntity = () => {
() => lighthouseApi.getWebsiteByUrl(websiteUrl),
[websiteUrl],
);
if (response.error) {
// Do not display error alert if its due to no audits found for a website
if (
response.error &&
!response.error.message.includes('no audited website found for url')
) {
errorApi.post(response.error);
}
return response;