diff --git a/plugins/catalog/src/api/CatalogClient.ts b/plugins/catalog/src/api/CatalogClient.ts index 65ee8a065e..3804315ace 100644 --- a/plugins/catalog/src/api/CatalogClient.ts +++ b/plugins/catalog/src/api/CatalogClient.ts @@ -70,9 +70,9 @@ export class CatalogClient implements CatalogApi { return await this.getOptional(`/locations/${id}`); } - async getEntities( + async getEntities( filter?: Record, - ): Promise { + ): Promise { let path = `/entities`; if (filter) { const params = new URLSearchParams(); diff --git a/plugins/catalog/src/api/types.ts b/plugins/catalog/src/api/types.ts index a523d97251..36c86f2e96 100644 --- a/plugins/catalog/src/api/types.ts +++ b/plugins/catalog/src/api/types.ts @@ -34,18 +34,13 @@ export interface CatalogApi { getEntityByName( compoundName: EntityCompoundName, ): Promise; - getEntities( - filter?: Record, - ): Promise; - addLocation( - type: string, - target: string, - ): Promise>; + getEntities(filter?: Record): Promise; + addLocation(type: string, target: string): Promise; getLocationByEntity(entity: Entity): Promise; removeEntityByUid(uid: string): Promise; } -export type AddLocationResponse = { +export type AddLocationResponse = { location: Location; - entities: EntityType[]; + entities: Entity[]; }; diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index 79c9b36143..403c9fed59 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -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", diff --git a/plugins/scaffolder/src/components/JobStatusModal/JobStatusModal.tsx b/plugins/scaffolder/src/components/JobStatusModal/JobStatusModal.tsx index a129d7c26a..c6ed55c92c 100644 --- a/plugins/scaffolder/src/components/JobStatusModal/JobStatusModal.tsx +++ b/plugins/scaffolder/src/components/JobStatusModal/JobStatusModal.tsx @@ -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 = ({ diff --git a/plugins/scaffolder/src/components/JobStatusModal/index.ts b/plugins/scaffolder/src/components/JobStatusModal/index.ts index 393cc8bfaf..5598999fe3 100644 --- a/plugins/scaffolder/src/components/JobStatusModal/index.ts +++ b/plugins/scaffolder/src/components/JobStatusModal/index.ts @@ -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'; diff --git a/plugins/scaffolder/src/components/MultistepJsonForm/index.ts b/plugins/scaffolder/src/components/MultistepJsonForm/index.ts index 6cb93273a6..fa28c5803b 100644 --- a/plugins/scaffolder/src/components/MultistepJsonForm/index.ts +++ b/plugins/scaffolder/src/components/MultistepJsonForm/index.ts @@ -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'; diff --git a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.test.tsx b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.test.tsx new file mode 100644 index 0000000000..6229a88fc3 --- /dev/null +++ b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.test.tsx @@ -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 = { + scaffold: jest.fn(), +}; + +const catalogApiMock = { + getEntities: jest.fn() as jest.MockedFunction, +}; +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( + + + , + ), + ); + + 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(res => { + resolve = res; + }); + catalogApiMock.getEntities.mockResolvedValueOnce(promise); + const rendered = await renderWithEffects( + wrapInTestApp( + + + , + ), + ); + + 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( + + + + + + + This is root} /> + + + , + ); + + expect( + rendered.queryByText('Create a new component'), + ).not.toBeInTheDocument(); + expect(rendered.queryByText('This is root')).toBeInTheDocument(); + }); +}); diff --git a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx index 5984d6f1c2..5c949e55d9 100644 --- a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx +++ b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx @@ -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({ + catalogApi.getEntities({ kind: 'Template', 'metadata.name': templateName, - }), + }) as Promise, ); return { template: data?.[0], loading: !error && !data, error }; }; @@ -97,7 +94,7 @@ export const TemplatePage = () => { setJobId(job); }; - const [entity, setEntity] = React.useState( + const [entity, setEntity] = React.useState( null, ); @@ -118,12 +115,9 @@ export const TemplatePage = () => { const { entities: [createdEntity], - } = await catalogApi.addLocation( - '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" /> - {loading && } + {loading && } {jobId && (