Add tests for components in the api-docs plugin

This commit is contained in:
Oliver Sand
2020-11-30 15:18:57 +01:00
parent 01180c65d7
commit 7d1d9a3df3
14 changed files with 609 additions and 67 deletions
@@ -0,0 +1,121 @@
/*
* 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 { ApiEntity } from '@backstage/catalog-model';
import { ApiProvider, ApiRegistry } from '@backstage/core';
import { renderInTestApp } from '@backstage/test-utils';
import { waitFor } from '@testing-library/react';
import React from 'react';
import { ApiDocsConfig, apiDocsConfigRef } from '../../config';
import { OpenApiDefinitionWidget } from '../OpenApiDefinitionWidget';
import { ApiDefinitionCard } from './ApiDefinitionCard';
describe('<ApiDefinitionCard />', () => {
const apiDocsConfig: jest.Mocked<ApiDocsConfig> = {
getApiDefinitionWidget: jest.fn(),
} as any;
let Wrapper: React.ComponentType;
beforeEach(() => {
const apis = ApiRegistry.with(apiDocsConfigRef, apiDocsConfig);
Wrapper = ({ children }: { children?: React.ReactNode }) => (
<ApiProvider apis={apis}>{children}</ApiProvider>
);
});
afterEach(() => jest.resetAllMocks());
it('renders API', async () => {
const definition = `
openapi: "3.0.0"
info:
version: 1.0.0
title: Artist API
license:
name: MIT
servers:
- url: http://artist.spotify.net/v1
paths:
/artists:
get:
summary: List all artists
responses:
"200":
description: Success
`;
const apiEntity: ApiEntity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'API',
metadata: {
name: 'my-name',
},
spec: {
type: 'openapi',
lifecycle: '...',
owner: '...',
definition,
},
};
apiDocsConfig.getApiDefinitionWidget.mockReturnValue({
type: 'openapi',
title: 'OpenAPI',
rawLanguage: 'yaml',
component: definition => (
<OpenApiDefinitionWidget definition={definition} />
),
});
const { getByText } = await renderInTestApp(
<Wrapper>
<ApiDefinitionCard apiEntity={apiEntity} />
</Wrapper>,
);
await waitFor(() => {
expect(getByText(/my-name/i)).toBeInTheDocument();
expect(getByText(/OpenAPI/)).toBeInTheDocument();
expect(getByText(/Raw/i)).toBeInTheDocument();
expect(getByText(/List all artists/i)).toBeInTheDocument();
});
});
it('fallback to plain view', async () => {
const apiEntity: ApiEntity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'API',
metadata: {
name: 'my-name',
},
spec: {
type: 'custom-type',
lifecycle: '...',
owner: '...',
definition: 'Custom Definition',
},
};
const { getByText } = await renderInTestApp(
<Wrapper>
<ApiDefinitionCard apiEntity={apiEntity} />
</Wrapper>,
);
expect(getByText(/my-name/i)).toBeInTheDocument();
expect(getByText(/custom-type/i)).toBeInTheDocument();
expect(getByText(/Custom Definition/i)).toBeInTheDocument();
});
});
@@ -15,50 +15,11 @@
*/
import { ApiEntity } from '@backstage/catalog-model';
import { CardTab, useApi, TabbedCard } from '@backstage/core';
import { CardTab, TabbedCard, useApi } from '@backstage/core';
import { Alert } from '@material-ui/lab';
import React from 'react';
import { apiDocsConfigRef } from '../../config';
import { PlainApiDefinitionWidget } from '../PlainApiDefinitionWidget';
import { OpenApiDefinitionWidget } from '../OpenApiDefinitionWidget';
import { AsyncApiDefinitionWidget } from '../AsyncApiDefinitionWidget';
import { GraphQlDefinitionWidget } from '../GraphQlDefinitionWidget';
export type ApiDefinitionWidget = {
type: string;
title: string;
component: (definition: string) => React.ReactElement;
rawLanguage?: string;
};
export function defaultDefinitionWidgets(): ApiDefinitionWidget[] {
return [
{
type: 'openapi',
title: 'OpenAPI',
rawLanguage: 'yaml',
component: definition => (
<OpenApiDefinitionWidget definition={definition} />
),
},
{
type: 'asyncapi',
title: 'AsyncAPI',
rawLanguage: 'yaml',
component: definition => (
<AsyncApiDefinitionWidget definition={definition} />
),
},
{
type: 'graphql',
title: 'GraphQL',
rawLanguage: 'graphql',
component: definition => (
<GraphQlDefinitionWidget definition={definition} />
),
},
];
}
type Props = {
apiEntity?: ApiEntity;
@@ -0,0 +1,55 @@
/*
* 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 { AsyncApiDefinitionWidget } from '../AsyncApiDefinitionWidget';
import { GraphQlDefinitionWidget } from '../GraphQlDefinitionWidget';
import { OpenApiDefinitionWidget } from '../OpenApiDefinitionWidget';
export type ApiDefinitionWidget = {
type: string;
title: string;
component: (definition: string) => React.ReactElement;
rawLanguage?: string;
};
export function defaultDefinitionWidgets(): ApiDefinitionWidget[] {
return [
{
type: 'openapi',
title: 'OpenAPI',
rawLanguage: 'yaml',
component: definition => (
<OpenApiDefinitionWidget definition={definition} />
),
},
{
type: 'asyncapi',
title: 'AsyncAPI',
rawLanguage: 'yaml',
component: definition => (
<AsyncApiDefinitionWidget definition={definition} />
),
},
{
type: 'graphql',
title: 'GraphQL',
rawLanguage: 'graphql',
component: definition => (
<GraphQlDefinitionWidget definition={definition} />
),
},
];
}
@@ -0,0 +1,92 @@
/*
* 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 { ApiEntity } from '@backstage/catalog-model';
import { ApiProvider, ApiRegistry } from '@backstage/core';
import { renderInTestApp } from '@backstage/test-utils';
import React from 'react';
import { ApiDocsConfig, apiDocsConfigRef } from '../../config';
import { ApiTypeTitle } from './ApiTypeTitle';
describe('<ApiTypeTitle />', () => {
const apiDocsConfig: jest.Mocked<ApiDocsConfig> = {
getApiDefinitionWidget: jest.fn(),
} as any;
let Wrapper: React.ComponentType;
beforeEach(() => {
const apis = ApiRegistry.with(apiDocsConfigRef, apiDocsConfig);
Wrapper = ({ children }: { children?: React.ReactNode }) => (
<ApiProvider apis={apis}>{children}</ApiProvider>
);
});
afterEach(() => jest.resetAllMocks());
it('renders API type title', async () => {
const apiEntity: ApiEntity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'API',
metadata: {
name: 'my-name',
},
spec: {
type: 'openapi',
lifecycle: '...',
owner: '...',
definition: '...',
},
};
apiDocsConfig.getApiDefinitionWidget.mockReturnValue({
type: 'openapi',
title: 'OpenAPI',
component: () => <div />,
});
const { getByText } = await renderInTestApp(
<Wrapper>
<ApiTypeTitle apiEntity={apiEntity} />
</Wrapper>,
);
expect(getByText(/OpenAPI/)).toBeInTheDocument();
});
it('fallback if title is unknown', async () => {
const apiEntity: ApiEntity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'API',
metadata: {
name: 'my-name',
},
spec: {
type: 'custom-type',
lifecycle: '...',
owner: '...',
definition: '...',
},
};
const { getByText } = await renderInTestApp(
<Wrapper>
<ApiTypeTitle apiEntity={apiEntity} />
</Wrapper>,
);
expect(getByText(/custom-type/i)).toBeInTheDocument();
});
});
@@ -0,0 +1,28 @@
/*
* 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 { ApiEntity } from '@backstage/catalog-model';
import { useApi } from '@backstage/core';
import React from 'react';
import { apiDocsConfigRef } from '../../config';
export const ApiTypeTitle = ({ apiEntity }: { apiEntity: ApiEntity }) => {
const config = useApi(apiDocsConfigRef);
const definition = config.getApiDefinitionWidget(apiEntity);
const type = definition ? definition.title : apiEntity.spec.type;
return <span>{type}</span>;
};
@@ -14,8 +14,7 @@
* limitations under the License.
*/
export type { ApiDefinitionWidget } from './ApiDefinitionCard';
export {
ApiDefinitionCard,
defaultDefinitionWidgets,
} from './ApiDefinitionCard';
export { ApiDefinitionCard } from './ApiDefinitionCard';
export { defaultDefinitionWidgets } from './ApiDefinitionWidget';
export type { ApiDefinitionWidget } from './ApiDefinitionWidget';
export { ApiTypeTitle } from './ApiTypeTitle';
@@ -20,23 +20,13 @@ import {
TableColumn,
TableFilter,
TableState,
useApi,
useQueryParamState,
} from '@backstage/core';
import { entityRoute, entityRouteParams } from '@backstage/plugin-catalog';
import { Chip, Link } from '@material-ui/core';
import { Chip } from '@material-ui/core';
import { Alert } from '@material-ui/lab';
import React from 'react';
import { generatePath, Link as RouterLink } from 'react-router-dom';
import { apiDocsConfigRef } from '../../config';
const ApiTypeTitle = ({ apiEntity }: { apiEntity: ApiEntityV1alpha1 }) => {
const config = useApi(apiDocsConfigRef);
const definition = config.getApiDefinitionWidget(apiEntity);
const type = definition ? definition.title : apiEntity.spec.type;
return <span>{type}</span>;
};
import { ApiTypeTitle } from '../ApiDefinitionCard';
import { EntityLink } from '../EntityLink';
const columns: TableColumn<Entity>[] = [
{
@@ -44,15 +34,7 @@ const columns: TableColumn<Entity>[] = [
field: 'metadata.name',
highlight: true,
render: (entity: any) => (
<Link
component={RouterLink}
to={generatePath(
`/catalog/${entityRoute.path}`,
entityRouteParams(entity),
)}
>
{entity.metadata.name}
</Link>
<EntityLink entity={entity}>{entity.metadata.name}</EntityLink>
),
},
{
@@ -0,0 +1,59 @@
/*
* 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 { renderInTestApp } from '@backstage/test-utils';
import React from 'react';
import { AsyncApiDefinitionWidget } from './AsyncApiDefinitionWidget';
describe('<AsyncApiDefinitionWidget />', () => {
it('renders asyncapi spec', async () => {
const definition = `
asyncapi: 2.0.0
info:
title: Account Service
version: 1.0.0
channels:
user/signedup:
subscribe:
message:
$ref: '#/components/messages/UserSignedUp'
components:
messages:
UserSignedUp:
payload:
type: object
properties:
displayName:
type: string
`;
const { getByText, getAllByText } = await renderInTestApp(
<AsyncApiDefinitionWidget definition={definition} />,
);
expect(getByText(/Account Service/i)).toBeInTheDocument();
expect(getByText(/user\/signedup/i)).toBeInTheDocument();
expect(getByText(/UserSignedUp/i)).toBeInTheDocument();
expect(getAllByText(/displayName/i)).toHaveLength(4);
});
it('renders error if definition is missing', async () => {
const { getByText } = await renderInTestApp(
<AsyncApiDefinitionWidget definition="" />,
);
expect(getByText(/Error/i)).toBeInTheDocument();
expect(getByText(/Document can't be null or falsey/i)).toBeInTheDocument();
});
});
@@ -0,0 +1,41 @@
/*
* 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';
import { renderInTestApp } from '@backstage/test-utils';
import React from 'react';
import { EntityLink } from './EntityLink';
describe('<EntityLink />', () => {
it('provides link to entity page', async () => {
const entity: Entity = {
apiVersion: 'v1',
kind: 'Component',
metadata: {
name: 'my-name',
namespace: 'my-namespace',
},
};
const { getByText } = await renderInTestApp(
<EntityLink entity={entity}>inner</EntityLink>,
);
expect(getByText(/inner/i)).toBeInTheDocument();
expect(getByText(/inner/i)).toHaveAttribute(
'href',
'/catalog/my-namespace/component/my-name',
);
});
});
@@ -0,0 +1,40 @@
/*
* 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';
import { entityRoute, entityRouteParams } from '@backstage/plugin-catalog';
import { Link } from '@material-ui/core';
import React, { PropsWithChildren } from 'react';
import { generatePath, Link as RouterLink } from 'react-router-dom';
type Props = {
entity: Entity;
};
// TODO: Could be useful for others too, as part of the catalog plugin
export const EntityLink = ({ entity, children }: PropsWithChildren<Props>) => {
return (
<Link
component={RouterLink}
to={generatePath(
`/catalog/${entityRoute.path}`,
entityRouteParams(entity),
)}
>
{children}
</Link>
);
};
@@ -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 { EntityLink } from './EntityLink';
@@ -0,0 +1,61 @@
/*
* 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 { renderInTestApp } from '@backstage/test-utils';
import React from 'react';
import { GraphQlDefinitionWidget } from './GraphQlDefinitionWidget';
describe('<GraphQlDefinitionWidget />', () => {
it('renders graphql schema', async () => {
const definition = `
"""Hello World!"""
schema {
query: Film
}
"""A single film."""
type Film {
"""The ID of an object"""
id: ID!
"""The title of this film."""
title: String
}
`;
// CodeMirror uses features that jsdom doesn't support so we have to patch
// it here, see: https://github.com/jsdom/jsdom/issues/3002
document.createRange = () => {
const range = new Range();
range.getBoundingClientRect = jest.fn();
range.getClientRects = () => {
return {
item: () => null,
length: 0,
[Symbol.iterator]: jest.fn(),
};
};
return range;
};
const { getByText } = await renderInTestApp(
<GraphQlDefinitionWidget definition={definition} />,
);
expect(getByText(/Film/i)).toBeInTheDocument();
});
});
@@ -0,0 +1,58 @@
/*
* 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 { renderInTestApp } from '@backstage/test-utils';
import { waitFor } from '@testing-library/react';
import React from 'react';
import { OpenApiDefinitionWidget } from './OpenApiDefinitionWidget';
describe('<OpenApiDefinitionWidget />', () => {
it('renders openapi spec', async () => {
const definition = `
openapi: "3.0.0"
info:
version: 1.0.0
title: Artist API
license:
name: MIT
servers:
- url: http://artist.spotify.net/v1
paths:
/artists:
get:
summary: List all artists
responses:
"200":
description: Success
`;
const { getByText } = await renderInTestApp(
<OpenApiDefinitionWidget definition={definition} />,
);
// swagger-ui loads the documentation asynchronously
await waitFor(() => {
expect(getByText(/\/artists/i)).toBeInTheDocument();
expect(getByText(/List all artists/i)).toBeInTheDocument();
});
});
it('renders error if definition is missing', async () => {
const { getByText } = await renderInTestApp(
<OpenApiDefinitionWidget definition="" />,
);
expect(getByText(/No API definition provided/i)).toBeInTheDocument();
});
});
@@ -0,0 +1,28 @@
/*
* 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 { renderInTestApp } from '@backstage/test-utils';
import React from 'react';
import { PlainApiDefinitionWidget } from './PlainApiDefinitionWidget';
describe('<PlainApiDefinitionWidget />', () => {
it('renders plain text', async () => {
const { getByText } = await renderInTestApp(
<PlainApiDefinitionWidget definition="Hello World" language="yaml" />,
);
expect(getByText(/Hello World/i)).toBeInTheDocument();
});
});