Add the fossa plugin

This commit is contained in:
Dominik Henneke
2020-12-10 16:06:23 +01:00
parent ebbc133eaa
commit ab88f2a255
18 changed files with 897 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
module.exports = {
extends: [require.resolve('@backstage/cli/config/eslint')],
};
+79
View File
@@ -0,0 +1,79 @@
# FOSSA Plugin
The FOSSA Plugin displays code statistics from [FOSSA](https://fossa.com/).
![FOSSA Card](./docs/fossa-card.png)
## Getting Started
1. Install the FOSSA Plugin:
```bash
# packages/app
yarn add @backstage/plugin-fossa
```
2. Add plugin to the app:
```js
// packages/app/src/plugins.ts
export { plugin as Fossa } from '@backstage/plugin-fossa';
```
3. Add the `FossaCard` to the EntityPage:
```jsx
// packages/app/src/components/catalog/EntityPage.tsx
import { FossaCard } from '@backstage/plugin-fossa';
const OverviewContent = ({ entity }: { entity: Entity }) => (
<Grid container spacing={3} alignItems="stretch">
// ...
<Grid item xs={12} sm={6} md={4}>
<FossaCard entity={entity} />
</Grid>
// ...
</Grid>
);
```
4. Add the proxy config:
```yaml
# app-config.yaml
proxy:
'/fossa':
target: https://app.fossa.io/api
allowedMethods: ['GET']
headers:
Authorization:
# Content: 'token <your-fossa-api-token>'
$env: FOSSA_AUTH_HEADER
# if you have a fossa organization, configure your id here
fossa:
organizationId: <your-fossa-organization-id>
```
5. Get an api-token and provide `FOSSA_AUTH_HEADER` as env variable (https://app.fossa.com/account/settings/integrations/api_tokens)
6. Add the `fossa.io/project-name` annotation to your catalog-info.yaml file:
```yaml
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: backstage
description: |
Backstage is an open-source developer portal that puts the developer experience first.
annotations:
fossa.io/project-name: YOUR_PROJECT_NAME
spec:
type: library
owner: CNCF
lifecycle: experimental
```
+25
View File
@@ -0,0 +1,25 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export interface Config {
fossa?: {
/**
* The organization id in fossa.
* @visibility frontend
*/
organizationId: string;
};
}
+133
View File
@@ -0,0 +1,133 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createDevApp } from '@backstage/dev-utils';
import {
Content,
createPlugin,
createRouteRef,
Header,
Page,
} from '@backstage/core';
import React from 'react';
import { Grid } from '@material-ui/core';
import { FossaApi, fossaApiRef } from '../src/api';
import { FossaCard } from '../src';
import { Entity } from '@backstage/catalog-model';
import { FOSSA_PROJECT_NAME_ANNOTATION } from '../src/components/useProjectName';
createDevApp()
.registerApi({
api: fossaApiRef,
deps: {},
factory: () =>
({
getFindingSummary: async projectTitle => {
switch (projectTitle) {
case 'error':
throw new Error('Error!');
case 'never':
return new Promise(() => {});
case 'zero-deps':
return {
timestamp: new Date().toISOString(),
issueCount: 0,
dependencyCount: 0,
projectDefaultBranch: 'master',
projectUrl: `/#${projectTitle}`,
};
case 'issues':
return {
timestamp: new Date().toISOString(),
issueCount: 5,
dependencyCount: 100,
projectDefaultBranch: 'develop',
projectUrl: `/#${projectTitle}`,
};
case 'no-issues':
return {
timestamp: new Date().toISOString(),
issueCount: 0,
dependencyCount: 150,
projectDefaultBranch: 'feat/fossa',
projectUrl: `/#${projectTitle}`,
};
default:
return undefined;
}
},
} as FossaApi),
})
.registerPlugin(
createPlugin({
id: 'fossa-demo',
register({ router }) {
const entity = (name?: string) =>
({
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
annotations: {
[FOSSA_PROJECT_NAME_ANNOTATION]: name,
},
name: name,
},
} as Entity);
const ExamplePage = () => (
<Page themeId="home">
<Header title="Fossa" />
<Content>
<Grid container>
<Grid item xs={12} sm={6} md={4}>
<FossaCard entity={entity('empty')} />
</Grid>
<Grid item xs={12} sm={6} md={4}>
<FossaCard entity={entity('error')} />
</Grid>
<Grid item xs={12} sm={6} md={4}>
<FossaCard entity={entity('never')} />
</Grid>
<Grid item xs={12} sm={6} md={4}>
<FossaCard entity={entity('zero-deps')} />
</Grid>
<Grid item xs={12} sm={6} md={4}>
<FossaCard entity={entity('issues')} />
</Grid>
<Grid item xs={12} sm={6} md={4}>
<FossaCard entity={entity('no-issues')} />
</Grid>
<Grid item xs={12}>
<FossaCard entity={entity(undefined)} />
</Grid>
</Grid>
</Content>
</Page>
);
router.addRoute(
createRouteRef({ path: '/', title: 'Fossa' }),
ExamplePage,
);
},
}),
)
.render();
Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

+62
View File
@@ -0,0 +1,62 @@
{
"name": "@backstage/plugin-fossa",
"version": "0.1.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
"private": false,
"publishConfig": {
"access": "public",
"main": "dist/index.esm.js",
"types": "dist/index.d.ts"
},
"homepage": "https://backstage.io",
"repository": {
"type": "git",
"url": "https://github.com/backstage/backstage",
"directory": "plugins/fossa"
},
"keywords": [
"backstage",
"fossa"
],
"scripts": {
"build": "backstage-cli plugin:build",
"start": "backstage-cli plugin:serve",
"lint": "backstage-cli lint",
"test": "backstage-cli test",
"diff": "backstage-cli plugin:diff",
"prepack": "backstage-cli prepack",
"postpack": "backstage-cli postpack",
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/catalog-model": "^0.5.0",
"@backstage/core": "^0.4.0",
"@backstage/theme": "^0.2.2",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
"cross-fetch": "^3.0.6",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-use": "^15.3.3"
},
"devDependencies": {
"@backstage/cli": "^0.4.1",
"@backstage/dev-utils": "^0.1.6",
"@backstage/test-utils": "^0.1.5",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
"@testing-library/user-event": "^12.0.7",
"@types/jest": "^26.0.7",
"@types/node": "^12.0.0",
"cross-fetch": "^3.0.6",
"msw": "^0.21.2"
},
"files": [
"dist",
"config.d.ts"
],
"configSchema": "config.d.ts"
}
+34
View File
@@ -0,0 +1,34 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createApiRef } from '@backstage/core';
export interface FindingSummary {
timestamp: string;
issueCount: number;
dependencyCount: number;
projectDefaultBranch: string;
projectUrl: string;
}
export const fossaApiRef = createApiRef<FossaApi>({
id: 'plugin.fossa.service',
description: 'Used by the Fossa plugin to make requests',
});
export type FossaApi = {
getFindingSummary(projectTitle?: string): Promise<FindingSummary | undefined>;
};
+138
View File
@@ -0,0 +1,138 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { UrlPatternDiscovery } from '@backstage/core';
import { msw } from '@backstage/test-utils';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import { FindingSummary, FossaApi, FossaClient } from './index';
const server = setupServer();
describe('FossaClient', () => {
msw.setupDefaultHandlers(server);
const mockBaseUrl = 'http://backstage:9191/api/proxy';
const discoveryApi = UrlPatternDiscovery.compile(mockBaseUrl);
let client: FossaApi;
beforeEach(() => {
client = new FossaClient({ discoveryApi, organizationId: '8736' });
});
it('should report finding summary', async () => {
server.use(
rest.get(`${mockBaseUrl}/fossa/projects`, (req, res, ctx) => {
expect(req.url.searchParams.toString()).toBe(
'count=1&title=our-service&organizationId=8736',
);
return res(
ctx.json([
{
locator: 'custom+8736/our-service',
default_branch: 'develop',
revisions: [
{
updatedAt: '2020-01-01T00:00:00Z',
dependency_count: 160,
unresolved_licensing_issue_count: 5,
unresolved_issue_count: 100,
},
],
},
]),
);
}),
);
const summary = await client.getFindingSummary('our-service');
expect(summary).toEqual({
timestamp: '2020-01-01T00:00:00Z',
issueCount: 5,
dependencyCount: 160,
projectDefaultBranch: 'develop',
projectUrl: 'https://app.fossa.com/projects/custom%2B8736%2Four-service',
} as FindingSummary);
});
it('should report finding summary without licenseing_issue_count', async () => {
server.use(
rest.get(`${mockBaseUrl}/fossa/projects`, (req, res, ctx) => {
expect(req.url.searchParams.toString()).toBe(
'count=1&title=our-service&organizationId=8736',
);
return res(
ctx.json([
{
locator: 'custom+8736/our-service',
default_branch: 'refs/master',
revisions: [
{
updatedAt: '2020-01-01T00:00:00Z',
dependency_count: 160,
unresolved_issue_count: 100,
},
],
},
]),
);
}),
);
const summary = await client.getFindingSummary('our-service');
expect(summary).toEqual({
timestamp: '2020-01-01T00:00:00Z',
issueCount: 100,
dependencyCount: 160,
projectDefaultBranch: 'refs/master',
projectUrl: 'https://app.fossa.com/projects/custom%2B8736%2Four-service',
} as FindingSummary);
});
it('should skip organizationId', async () => {
client = new FossaClient({ discoveryApi });
server.use(
rest.get(`${mockBaseUrl}/fossa/projects`, (req, res, ctx) => {
expect(req.url.searchParams.toString()).toBe(
'count=1&title=our-service',
);
return res(ctx.status(404));
}),
);
const summary = await client.getFindingSummary('our-service');
expect(summary).toBeUndefined();
});
it('should handle 404 status', async () => {
server.use(
rest.get(`${mockBaseUrl}/fossa/projects`, (req, res, ctx) => {
expect(req.url.searchParams.toString()).toBe(
'count=1&title=our-service&organizationId=8736',
);
return res(ctx.status(404));
}),
);
const summary = await client.getFindingSummary('our-service');
expect(summary).toBeUndefined();
});
});
+74
View File
@@ -0,0 +1,74 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { DiscoveryApi } from '@backstage/core';
import fetch from 'cross-fetch';
import { FindingSummary, FossaApi } from './FossaApi';
export class FossaClient implements FossaApi {
discoveryApi: DiscoveryApi;
organizationId?: string;
constructor({
discoveryApi,
organizationId,
}: {
discoveryApi: DiscoveryApi;
organizationId?: string;
}) {
this.discoveryApi = discoveryApi;
this.organizationId = organizationId;
}
private async callApi(path: string): Promise<any> {
const apiUrl = `${await this.discoveryApi.getBaseUrl('proxy')}/fossa`;
const response = await fetch(`${apiUrl}/${path}`);
if (response.status === 200) {
return await response.json();
}
return undefined;
}
async getFindingSummary(
projectTitle?: string,
): Promise<FindingSummary | undefined> {
if (!projectTitle) {
return undefined;
}
const project = await this.callApi(
`projects?count=1&title=${projectTitle}${
this.organizationId ? `&organizationId=${this.organizationId}` : ''
}`,
);
if (!project) {
return undefined;
}
const revision = project[0].revisions[0];
return {
timestamp: revision.updatedAt,
issueCount:
revision.unresolved_licensing_issue_count ||
revision.unresolved_issue_count,
dependencyCount: revision.dependency_count,
projectDefaultBranch: project[0].default_branch,
projectUrl: `https://app.fossa.com/projects/${encodeURIComponent(
project[0].locator,
)}`,
};
}
}
+19
View File
@@ -0,0 +1,19 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export type { FossaApi, FindingSummary } from './FossaApi';
export { fossaApiRef } from './FossaApi';
export { FossaClient } from './FossaClient';
@@ -0,0 +1,177 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import {
EmptyState,
InfoCard,
MissingAnnotationEmptyState,
Progress,
useApi,
} from '@backstage/core';
import { useAsync } from 'react-use';
import { Entity } from '@backstage/catalog-model';
import { fossaApiRef } from '../../api';
import { makeStyles } from '@material-ui/core/styles';
import {
FOSSA_PROJECT_NAME_ANNOTATION,
useProjectName,
} from '../useProjectName';
import { Grid, Tooltip } from '@material-ui/core';
const useStyles = makeStyles(theme => ({
numberError: {
fontSize: '5rem',
textAlign: 'center',
fontWeight: theme.typography.fontWeightMedium,
margin: theme.spacing(2, 0),
color: theme.palette.error.main,
},
numberSuccess: {
fontSize: '5rem',
textAlign: 'center',
fontWeight: theme.typography.fontWeightMedium,
margin: theme.spacing(2, 0),
color: theme.palette.success.main,
},
description: {
fontSize: '1rem',
textAlign: 'center',
fontWeight: theme.typography.fontWeightMedium,
color: theme.palette.text.secondary,
},
disabled: {
backgroundColor: theme.palette.background.default,
},
lastAnalyzed: {
color: theme.palette.text.secondary,
textAlign: 'center',
},
branch: {
textDecoration: 'underline dotted',
},
}));
export const FossaCard = ({
entity,
variant = 'gridItem',
}: {
entity: Entity;
variant?: string;
}) => {
const fossaApi = useApi(fossaApiRef);
const projectTitle = useProjectName(entity);
const { value, loading } = useAsync(
async () => fossaApi.getFindingSummary(projectTitle),
[fossaApi, projectTitle],
);
const deepLink = value
? {
title: 'View more',
link: value.projectUrl,
}
: undefined;
const classes = useStyles();
return (
<>
<InfoCard
title="License Findings"
deepLink={deepLink}
variant={variant}
className={
!loading && (!projectTitle || !value) ? classes.disabled : undefined
}
>
{loading && <Progress />}
{!loading && !projectTitle && (
<MissingAnnotationEmptyState
annotation={FOSSA_PROJECT_NAME_ANNOTATION}
/>
)}
{!loading && projectTitle && !value && (
<EmptyState
missing="info"
title="No information to display"
description={`There is no Fossa project with title '${projectTitle}'.`}
/>
)}
{value && (
<Grid
item
container
direction="column"
justify="space-between"
alignItems="center"
style={{ height: '100%' }}
spacing={0}
>
<Grid item>
<p
className={
value.issueCount > 0 || value.dependencyCount === 0
? classes.numberError
: classes.numberSuccess
}
>
{value.issueCount}
</p>
{value.dependencyCount > 0 && (
<p className={classes.description}>Number of issues</p>
)}
{value.dependencyCount === 0 && (
<p className={classes.description}>
No Dependencies.
<br />
Please check your FOSSA project settings.
</p>
)}
</Grid>
<Grid item className={classes.lastAnalyzed}>
Last analyzed on{' '}
{new Date(value.timestamp).toLocaleString('en-US', {
timeZone: 'UTC',
day: 'numeric',
month: 'short',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
hour12: false,
})}
</Grid>
<Grid item className={classes.lastAnalyzed}>
Based on {value.dependencyCount} Dependencies on branch{' '}
<Tooltip title="The default branch can be changed by a FOSSA admin.">
<span className={classes.branch}>
{value.projectDefaultBranch}
</span>
</Tooltip>
.
</Grid>
</Grid>
)}
</InfoCard>
</>
);
};
@@ -0,0 +1,17 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { FossaCard } from './FossaCard';
+17
View File
@@ -0,0 +1,17 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './FossaCard';
@@ -0,0 +1,23 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Entity } from '@backstage/catalog-model';
export const FOSSA_PROJECT_NAME_ANNOTATION = 'fossa.io/project-name';
export const useProjectName = (entity: Entity) => {
return entity?.metadata.annotations?.[FOSSA_PROJECT_NAME_ANNOTATION] ?? '';
};
+18
View File
@@ -0,0 +1,18 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { plugin } from './plugin';
export * from './components';
+23
View File
@@ -0,0 +1,23 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { plugin } from './plugin';
describe('fossa', () => {
it('should export plugin', () => {
expect(plugin).toBeDefined();
});
});
+38
View File
@@ -0,0 +1,38 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
configApiRef,
createApiFactory,
createPlugin,
discoveryApiRef,
} from '@backstage/core';
import { fossaApiRef, FossaClient } from './api';
export const plugin = createPlugin({
id: 'fossa',
apis: [
createApiFactory({
api: fossaApiRef,
deps: { configApi: configApiRef, discoveryApi: discoveryApiRef },
factory: ({ configApi, discoveryApi }) =>
new FossaClient({
discoveryApi,
organizationId: configApi.getOptionalString('fossa.organizationId'),
}),
}),
],
});
+17
View File
@@ -0,0 +1,17 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import '@testing-library/jest-dom';