test(scaffolder): add tests for TemplatePage

This commit is contained in:
Ivan Shmidt
2020-07-01 23:26:45 +02:00
parent 6aee52bd8c
commit fbcc0db2db
8 changed files with 200 additions and 26 deletions
+2 -2
View File
@@ -70,9 +70,9 @@ export class CatalogClient implements CatalogApi {
return await this.getOptional(`/locations/${id}`);
}
async getEntities<EntityType extends Entity = Entity>(
async getEntities(
filter?: Record<string, string | string[]>,
): Promise<EntityType[]> {
): Promise<Entity[]> {
let path = `/entities`;
if (filter) {
const params = new URLSearchParams();
+4 -9
View File
@@ -34,18 +34,13 @@ export interface CatalogApi {
getEntityByName(
compoundName: EntityCompoundName,
): Promise<Entity | undefined>;
getEntities<EntityType extends Entity = Entity>(
filter?: Record<string, string | string[]>,
): Promise<EntityType[]>;
addLocation<EntityType extends Entity = Entity>(
type: string,
target: string,
): Promise<AddLocationResponse<EntityType>>;
getEntities(filter?: Record<string, string | string[]>): Promise<Entity[]>;
addLocation(type: string, target: string): Promise<AddLocationResponse>;
getLocationByEntity(entity: Entity): Promise<Location | undefined>;
removeEntityByUid(uid: string): Promise<void>;
}
export type AddLocationResponse<EntityType extends Entity = Entity> = {
export type AddLocationResponse = {
location: Location;
entities: EntityType[];
entities: Entity[];
};
+1
View File
@@ -43,6 +43,7 @@
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.12",
"@backstage/dev-utils": "^0.1.1-alpha.12",
"@backstage/test-utils": "^0.1.1-alpha.12",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
"@testing-library/user-event": "^12.0.7",
@@ -24,7 +24,7 @@ import {
import { JobStage } from '../JobStage/JobStage';
import { useJobPolling } from './useJobPolling';
import { Job } from '../../types';
import { ComponentEntityV1alpha1 } from '@backstage/catalog-model';
import { TemplateEntityV1alpha1 } from '@backstage/catalog-model';
import { Button } from '@backstage/core';
import { entityRoute } from '@backstage/plugin-catalog';
import { generatePath } from 'react-router-dom';
@@ -33,7 +33,7 @@ type Props = {
onClose: () => void;
onComplete: (job: Job) => void;
jobId: string;
entity: ComponentEntityV1alpha1 | null;
entity: TemplateEntityV1alpha1 | null;
};
export const JobStatusModal = ({
@@ -1 +1,16 @@
/*
* 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 { JobStatusModal } from './JobStatusModal';
@@ -1 +1,16 @@
/*
* 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 { MultistepJsonForm } from './MultistepJsonForm';
@@ -0,0 +1,154 @@
/*
* 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 { TemplatePage } from './TemplatePage';
import { wrapInTestApp, renderWithEffects } from '@backstage/test-utils';
import { ApiRegistry, errorApiRef, ApiProvider } from '@backstage/core';
import { scaffolderApiRef, ScaffolderApi } from '../../api';
import { catalogApiRef, CatalogApi } from '@backstage/plugin-catalog';
import { mutate } from 'swr';
import { act } from 'react-dom/test-utils';
import { Route, MemoryRouter } from 'react-router';
import { rootRoute } from '../../routes';
import { ThemeProvider } from '@material-ui/core';
import { lightTheme } from '@backstage/theme';
const templateMock = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Template',
metadata: {
annotations: {
'backstage.io/managed-by-location':
'file:/something/sample-templates/react-ssr-template/template.yaml',
},
name: 'react-ssr-template',
title: 'React SSR Template',
description:
'Next.js application skeleton for creating isomorphic web applications.',
tags: ['Recommended', 'React'],
uid: '55efc748-4a2b-460f-9e47-3f4fd23b46f7',
etag: 'MTM3YThjY2QtYTc1MS00MTFkLTk3YTAtNzgyMDg3MDVmZTVm',
generation: 1,
},
spec: {
processor: 'cookiecutter',
type: 'website',
path: '.',
schema: {
required: ['component_id', 'description'],
properties: {
component_id: {
title: 'Name',
type: 'string',
description: 'Unique name of the component',
},
description: {
title: 'Description',
type: 'string',
description: 'Description of the component',
},
},
},
},
};
jest.mock('react-router-dom', () => {
return {
...(jest.requireActual('react-router-dom') as any),
useParams: () => ({
templateName: 'test',
}),
};
});
const scaffolderApiMock: Partial<ScaffolderApi> = {
scaffold: jest.fn(),
};
const catalogApiMock = {
getEntities: jest.fn() as jest.MockedFunction<CatalogApi['getEntities']>,
};
const errorApiMock = { post: jest.fn(), error$: jest.fn() };
const apis = ApiRegistry.from([
[scaffolderApiRef, scaffolderApiMock],
[errorApiRef, errorApiMock],
[catalogApiRef, catalogApiMock],
]);
describe('TemplatePage', () => {
afterEach(async () => {
// Cleaning up swr's cache
await act(async () => {
await mutate('templates/test');
});
});
it('renders correctly', async () => {
catalogApiMock.getEntities.mockResolvedValueOnce([templateMock]);
const rendered = await renderWithEffects(
wrapInTestApp(
<ApiProvider apis={apis}>
<TemplatePage />
</ApiProvider>,
),
);
expect(rendered.queryByText('Create a new component')).toBeInTheDocument();
expect(rendered.queryByText('React SSR Template')).toBeInTheDocument();
// await act(async () => await mutate('templates/test'));
});
it('renders spinner while loading', async () => {
let resolve: Function;
const promise = new Promise<any>(res => {
resolve = res;
});
catalogApiMock.getEntities.mockResolvedValueOnce(promise);
const rendered = await renderWithEffects(
wrapInTestApp(
<ApiProvider apis={apis}>
<TemplatePage />
</ApiProvider>,
),
);
expect(rendered.queryByText('Create a new component')).toBeInTheDocument();
expect(rendered.queryByTestId('loading-progress')).toBeInTheDocument();
// Need to cleanup the promise or will timeout
resolve!();
});
it('navigates away if no template was loaded', async () => {
catalogApiMock.getEntities.mockResolvedValueOnce([]);
const rendered = await renderWithEffects(
<ApiProvider apis={apis}>
<ThemeProvider theme={lightTheme}>
<MemoryRouter initialEntries={['/create/test']}>
<Route path="/create/test">
<TemplatePage />
</Route>
<Route path={rootRoute.path} element={<>This is root</>} />
</MemoryRouter>
</ThemeProvider>
</ApiProvider>,
);
expect(
rendered.queryByText('Create a new component'),
).not.toBeInTheDocument();
expect(rendered.queryByText('This is root')).toBeInTheDocument();
});
});
@@ -13,10 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
ComponentEntityV1alpha1,
TemplateEntityV1alpha1,
} from '@backstage/catalog-model';
import { TemplateEntityV1alpha1 } from '@backstage/catalog-model';
import {
Content,
errorApiRef,
@@ -46,10 +43,10 @@ const useTemplate = (
const { data, error } = useStaleWhileRevalidate(
`templates/${templateName}`,
async () =>
catalogApi.getEntities<TemplateEntityV1alpha1>({
catalogApi.getEntities({
kind: 'Template',
'metadata.name': templateName,
}),
}) as Promise<TemplateEntityV1alpha1[]>,
);
return { template: data?.[0], loading: !error && !data, error };
};
@@ -97,7 +94,7 @@ export const TemplatePage = () => {
setJobId(job);
};
const [entity, setEntity] = React.useState<ComponentEntityV1alpha1 | null>(
const [entity, setEntity] = React.useState<TemplateEntityV1alpha1 | null>(
null,
);
@@ -118,12 +115,9 @@ export const TemplatePage = () => {
const {
entities: [createdEntity],
} = await catalogApi.addLocation<ComponentEntityV1alpha1>(
'github',
componentYaml,
);
} = await catalogApi.addLocation('github', componentYaml);
setEntity(createdEntity);
setEntity((createdEntity as any) as TemplateEntityV1alpha1);
};
if (!loading && !template) {
@@ -152,7 +146,7 @@ export const TemplatePage = () => {
subtitle="Create new software components using standard templates"
/>
<Content>
{loading && <LinearProgress />}
{loading && <LinearProgress data-testid="loading-progress" />}
{jobId && (
<JobStatusModal
onComplete={handleCreateComplete}