diff --git a/.changeset/fluffy-dolphins-battle.md b/.changeset/fluffy-dolphins-battle.md new file mode 100644 index 0000000000..b247d549e5 --- /dev/null +++ b/.changeset/fluffy-dolphins-battle.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': patch +--- + +Add tests for the new pages header navigation. diff --git a/plugins/scaffolder/src/alpha/components/TemplateEditorPage/CustomFieldExplorer.tsx b/plugins/scaffolder/src/alpha/components/TemplateEditorPage/CustomFieldExplorer.tsx index 0dacb00271..5ae44ba868 100644 --- a/plugins/scaffolder/src/alpha/components/TemplateEditorPage/CustomFieldExplorer.tsx +++ b/plugins/scaffolder/src/alpha/components/TemplateEditorPage/CustomFieldExplorer.tsx @@ -88,27 +88,28 @@ export const CustomFieldExplorer = ({ const classes = useStyles(); const { t } = useTranslationRef(scaffolderTranslationRef); const fieldOptions = customFieldExtensions.filter(field => !!field.schema); - const [selectedField, setSelectedField] = useState(fieldOptions[0]); + const [selectedField, setSelectedField] = useState(fieldOptions?.[0]); const [fieldFormState, setFieldFormState] = useState({}); const [refreshKey, setRefreshKey] = useState(Date.now()); - const sampleFieldTemplate = useMemo( - () => - yaml.stringify({ - parameters: [ - { - title: `${selectedField.name} Example`, - properties: { - [selectedField.name]: { - type: selectedField.schema?.returnValue?.type, - 'ui:field': selectedField.name, - 'ui:options': fieldFormState, - }, + const sampleFieldTemplate = useMemo(() => { + if (!selectedField) { + return ''; + } + return yaml.stringify({ + parameters: [ + { + title: `${selectedField.name} Example`, + properties: { + [selectedField.name]: { + type: selectedField.schema?.returnValue?.type, + 'ui:field': selectedField.name, + 'ui:options': fieldFormState, }, }, - ], - }), - [fieldFormState, selectedField], - ); + }, + ], + }); + }, [fieldFormState, selectedField]); const fieldComponents = useMemo(() => { return Object.fromEntries( @@ -185,7 +186,7 @@ export const CustomFieldExplorer = ({ formContext={{ fieldFormState }} onSubmit={e => handleFieldConfigChange(e.formData)} validator={validator} - schema={selectedField.schema?.uiOptions || {}} + schema={selectedField?.schema?.uiOptions || {}} experimental_defaultFormStateBehavior={{ allOf: 'populateDefaults', }} @@ -194,7 +195,7 @@ export const CustomFieldExplorer = ({ variant="contained" color="primary" type="submit" - disabled={!selectedField.schema?.uiOptions} + disabled={!selectedField?.schema?.uiOptions} > {t( 'templateEditorPage.customFieldExplorer.fieldForm.applyButtonTitle', diff --git a/plugins/scaffolder/src/alpha/components/TemplateEditorPage/CustomFieldsPage.test.tsx b/plugins/scaffolder/src/alpha/components/TemplateEditorPage/CustomFieldsPage.test.tsx new file mode 100644 index 0000000000..a6c5ff89d3 --- /dev/null +++ b/plugins/scaffolder/src/alpha/components/TemplateEditorPage/CustomFieldsPage.test.tsx @@ -0,0 +1,71 @@ +/* + * Copyright 2024 The Backstage Authors + * + * 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 { screen } from '@testing-library/react'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; +import { catalogApiRef } from '@backstage/plugin-catalog-react'; +import { scaffolderApiRef } from '@backstage/plugin-scaffolder-react'; +import { rootRouteRef } from '../../../routes'; +import { CustomFieldsPage } from './CustomFieldsPage'; + +describe('CustomFieldsPage', () => { + const catalogApiMock = { getEntities: jest.fn().mockResolvedValue([]) }; + const scaffolderApiMock = {}; + + it('Should render without exploding', async () => { + await renderInTestApp( + + + , + { + mountedRoutes: { + '/': rootRouteRef, + }, + }, + ); + expect( + screen.getByRole('heading', { name: 'Custom Field Explorer' }), + ).toBeInTheDocument(); + }); + + it('Should have an link back to the edit page', async () => { + await renderInTestApp( + + + , + { + mountedRoutes: { + '/': rootRouteRef, + }, + routeEntries: ['/edit'], + }, + ); + expect( + screen.getByRole('link', { name: /Manage Templates/ }), + ).toHaveAttribute('href', '/edit'); + }); +}); diff --git a/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateEditorPage.test.tsx b/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateEditorPage.test.tsx new file mode 100644 index 0000000000..7f7c66bc5d --- /dev/null +++ b/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateEditorPage.test.tsx @@ -0,0 +1,71 @@ +/* + * Copyright 2024 The Backstage Authors + * + * 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 { screen } from '@testing-library/react'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; +import { catalogApiRef } from '@backstage/plugin-catalog-react'; +import { scaffolderApiRef } from '@backstage/plugin-scaffolder-react'; +import { TemplateEditorPage } from './TemplateEditorPage'; +import { rootRouteRef } from '../../../routes'; + +describe('TemplateEditorPage', () => { + const catalogApiMock = { getEntities: jest.fn().mockResolvedValue([]) }; + const scaffolderApiMock = {}; + + it('Should render without exploding', async () => { + await renderInTestApp( + + + , + { + mountedRoutes: { + '/': rootRouteRef, + }, + }, + ); + expect( + screen.getByRole('heading', { name: 'Template Editor' }), + ).toBeInTheDocument(); + }); + + it('Should have an link back to the edit page', async () => { + await renderInTestApp( + + + , + { + mountedRoutes: { + '/': rootRouteRef, + }, + routeEntries: ['/edit'], + }, + ); + expect( + screen.getByRole('link', { name: /Manage Templates/ }), + ).toHaveAttribute('href', '/edit'); + }); +}); diff --git a/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateFormPage.test.tsx b/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateFormPage.test.tsx new file mode 100644 index 0000000000..35e2772873 --- /dev/null +++ b/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateFormPage.test.tsx @@ -0,0 +1,59 @@ +/* + * Copyright 2024 The Backstage Authors + * + * 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 { screen } from '@testing-library/react'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; +import { TemplateFormPage } from './TemplateFormPage'; +import { rootRouteRef } from '../../../routes'; +import { catalogApiRef } from '@backstage/plugin-catalog-react'; + +describe('TemplateFormPage', () => { + const catalogApiMock = { getEntities: jest.fn().mockResolvedValue([]) }; + + it('Should render without exploding', async () => { + await renderInTestApp( + + + , + { + mountedRoutes: { + '/': rootRouteRef, + }, + }, + ); + expect( + screen.getByRole('heading', { name: 'Template Editor' }), + ).toBeInTheDocument(); + }); + + it('Should have an link back to the edit page', async () => { + await renderInTestApp( + + + , + { + mountedRoutes: { + '/': rootRouteRef, + }, + routeEntries: ['/edit'], + }, + ); + expect( + screen.getByRole('link', { name: /Manage Templates/ }), + ).toHaveAttribute('href', '/edit'); + }); +}); diff --git a/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateIntroPage.test.tsx b/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateIntroPage.test.tsx new file mode 100644 index 0000000000..f85c37233e --- /dev/null +++ b/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateIntroPage.test.tsx @@ -0,0 +1,91 @@ +/* + * Copyright 2024 The Backstage Authors + * + * 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 { screen } from '@testing-library/react'; +import { renderInTestApp } from '@backstage/test-utils'; +import { TemplateIntroPage } from './TemplateIntroPage'; +import { rootRouteRef } from '../../../routes'; + +describe('TemplateIntroPage', () => { + it('Should render without exploding', async () => { + await renderInTestApp(, { + mountedRoutes: { + '/': rootRouteRef, + }, + }); + expect( + screen.getByRole('heading', { name: 'Manage Templates' }), + ).toBeInTheDocument(); + }); + + it('Should have an link back to the create page', async () => { + await renderInTestApp(, { + mountedRoutes: { + '/': rootRouteRef, + }, + }); + expect(screen.getByRole('link', { name: /Scaffolder/ })).toHaveAttribute( + 'href', + '/', + ); + }); + + it('Should have an action to load a template directory', async () => { + await renderInTestApp(, { + mountedRoutes: { + '/': rootRouteRef, + }, + }); + expect( + screen.getByRole('button', { name: /Load Template Directory/ }), + ).toBeInTheDocument(); + }); + + it('Should have an action to create a template directory', async () => { + await renderInTestApp(, { + mountedRoutes: { + '/': rootRouteRef, + }, + }); + expect( + screen.getByRole('button', { name: /Create New Template/ }), + ).toBeInTheDocument(); + }); + + it('Should have an action to open the template playground', async () => { + await renderInTestApp(, { + mountedRoutes: { + '/': rootRouteRef, + }, + }); + expect( + screen.getByRole('button', { name: /Template Form Playground/ }), + ).toBeInTheDocument(); + }); + + it('Should have an action to open the custom fields explorer', async () => { + await renderInTestApp(, { + mountedRoutes: { + '/': rootRouteRef, + }, + }); + + expect( + screen.getByRole('button', { name: /Custom Field Explorer/ }), + ).toBeInTheDocument(); + }); +}); diff --git a/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateIntroPage.tsx b/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateIntroPage.tsx index 69fcfd37bf..28e4969718 100644 --- a/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateIntroPage.tsx +++ b/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateIntroPage.tsx @@ -13,11 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React from 'react'; +import React, { useCallback } from 'react'; import { Content, Header, Page } from '@backstage/core-components'; -import { WebFileSystemAccess } from '../../../lib/filesystem'; - import { TemplateEditorIntro } from './TemplateEditorIntro'; import { useNavigate } from 'react-router-dom'; import { useRouteRef } from '@backstage/core-plugin-api'; @@ -29,8 +27,7 @@ import { } from '../../../routes'; import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; import { scaffolderTranslationRef } from '../../../translation'; -import { WebFileSystemStore } from '../../../lib/filesystem/WebFileSystemStore'; -import { createExampleTemplate } from '../../../lib/filesystem/createExampleTemplate'; +import { useTemplateDirectory } from './useTemplateDirectory'; export function TemplateIntroPage() { const navigate = useNavigate(); @@ -39,6 +36,33 @@ export function TemplateIntroPage() { const templateFormLink = useRouteRef(templateFormRouteRef); const customFieldsLink = useRouteRef(customFieldsRouteRef); const { t } = useTranslationRef(scaffolderTranslationRef); + const { openDirectory, createDirectory } = useTemplateDirectory(); + + const handleSelect = useCallback( + (option: 'create-template' | 'local' | 'form' | 'field-explorer') => { + if (option === 'local') { + openDirectory() + .then(() => navigate(editorLink())) + .catch(() => {}); + } else if (option === 'create-template') { + createDirectory() + .then(() => navigate(editorLink())) + .catch(() => {}); + } else if (option === 'form') { + navigate(templateFormLink()); + } else if (option === 'field-explorer') { + navigate(customFieldsLink()); + } + }, + [ + openDirectory, + createDirectory, + navigate, + editorLink, + templateFormLink, + customFieldsLink, + ], + ); return ( @@ -49,29 +73,7 @@ export function TemplateIntroPage() { subtitle={t('templateIntroPage.subtitle')} /> - { - if (option === 'local') { - WebFileSystemAccess.requestDirectoryAccess() - .then(directory => WebFileSystemStore.setDirectory(directory)) - .then(() => navigate(editorLink())) - .catch(() => {}); - } else if (option === 'create-template') { - WebFileSystemAccess.requestDirectoryAccess() - .then(directory => { - createExampleTemplate(directory).then(() => { - WebFileSystemStore.setDirectory(directory); - navigate(editorLink()); - }); - }) - .catch(() => {}); - } else if (option === 'form') { - navigate(templateFormLink()); - } else if (option === 'field-explorer') { - navigate(customFieldsLink()); - } - }} - /> + );