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());
- }
- }}
- />
+
);