diff --git a/.changeset/twenty-cups-knock.md b/.changeset/twenty-cups-knock.md new file mode 100644 index 0000000000..c78c0528a4 --- /dev/null +++ b/.changeset/twenty-cups-knock.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': patch +--- + +Add tests for the `TemplateEditorToolbarFilesMenu` component. diff --git a/plugins/scaffolder/report-alpha.api.md b/plugins/scaffolder/report-alpha.api.md index 9acf99f827..2dcef12726 100644 --- a/plugins/scaffolder/report-alpha.api.md +++ b/plugins/scaffolder/report-alpha.api.md @@ -382,7 +382,7 @@ export type TemplateWizardPageProps = { // Warnings were encountered during analysis: // // src/alpha/components/TemplateEditorPage/CustomFieldExplorer.d.ts:4:1 - (ae-undocumented) Missing documentation for "ScaffolderCustomFieldExplorerClassKey". -// src/alpha/components/TemplateEditorPage/TemplateEditor.d.ts:5:1 - (ae-undocumented) Missing documentation for "ScaffolderTemplateEditorClassKey". +// src/alpha/components/TemplateEditorPage/TemplateEditor.d.ts:4:1 - (ae-undocumented) Missing documentation for "ScaffolderTemplateEditorClassKey". // src/alpha/components/TemplateEditorPage/TemplateFormPreviewer.d.ts:4:1 - (ae-undocumented) Missing documentation for "ScaffolderTemplateFormPreviewerClassKey". // src/alpha/components/TemplateListPage/TemplateListPage.d.ts:7:1 - (ae-undocumented) Missing documentation for "TemplateListPageProps". // src/alpha/components/TemplateWizardPage/TemplateWizardPage.d.ts:6:1 - (ae-undocumented) Missing documentation for "TemplateWizardPageProps". diff --git a/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateEditor.tsx b/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateEditor.tsx index f2b9e5d7e2..df941c5931 100644 --- a/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateEditor.tsx +++ b/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateEditor.tsx @@ -13,12 +13,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React, { useState } from 'react'; +import React, { useCallback, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; + +import { useRouteRef } from '@backstage/core-plugin-api'; import type { FormProps, LayoutOptions, + FieldExtensionOptions, } from '@backstage/plugin-scaffolder-react'; -import { FieldExtensionOptions } from '@backstage/plugin-scaffolder-react'; + +import { editRouteRef } from '../../../routes'; + +import { useTemplateDirectory } from './useTemplateDirectory'; import { DirectoryEditorProvider } from './DirectoryEditorContext'; import { TemplateEditorLayout, @@ -31,11 +38,10 @@ import { import { TemplateEditorToolbar } from './TemplateEditorToolbar'; import { TemplateEditorToolbarFileMenu } from './TemplateEditorToolbarFileMenu'; import { TemplateEditorBrowser } from './TemplateEditorBrowser'; -import { DryRunProvider } from './DryRunContext'; import { TemplateEditorTextArea } from './TemplateEditorTextArea'; import { TemplateEditorForm } from './TemplateEditorForm'; +import { DryRunProvider } from './DryRunContext'; import { DryRunResults } from './DryRunResults'; -import { useTemplateDirectory } from './useTemplateDirectory'; /** @public */ export type ScaffolderTemplateEditorClassKey = @@ -53,13 +59,19 @@ export const TemplateEditor = (props: { }) => { const { layouts, formProps, fieldExtensions } = props; const [errorText, setErrorText] = useState(); + const navigate = useNavigate(); + const editLink = useRouteRef(editRouteRef); const { directory, - handleOpenDirectory, - handleCreateDirectory, - handleCloseDirectory, + openDirectory: handleOpenDirectory, + createDirectory: handleCreateDirectory, + closeDirectory, } = useTemplateDirectory(); + const handleCloseDirectory = useCallback(() => { + closeDirectory().then(() => navigate(editLink())); + }, [closeDirectory, navigate, editLink]); + return ( @@ -68,13 +80,13 @@ export const TemplateEditor = (props: { - + diff --git a/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateEditorToolbarFileMenu.test.tsx b/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateEditorToolbarFileMenu.test.tsx new file mode 100644 index 0000000000..6483b9acff --- /dev/null +++ b/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateEditorToolbarFileMenu.test.tsx @@ -0,0 +1,153 @@ +/* + * 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 userEvent from '@testing-library/user-event'; +import { renderInTestApp } from '@backstage/test-utils'; +import { TemplateEditorToolbarFileMenu } from './TemplateEditorToolbarFileMenu'; +import { rootRouteRef } from '../../../routes'; + +describe('TemplateEditorToolbarFileMenu', () => { + it('should disable open directory by default', async () => { + await renderInTestApp(, { + mountedRoutes: { + '/': rootRouteRef, + }, + }); + + expect( + screen.queryByRole('menuitem', { name: 'Open template directory' }), + ).not.toBeInTheDocument(); + + await userEvent.click(screen.getByRole('button', { name: 'File' })); + + expect( + screen.getByRole('menuitem', { name: 'Open template directory' }), + ).toHaveAttribute('aria-disabled', 'true'); + }); + + it('should disable create directory by default', async () => { + await renderInTestApp(, { + mountedRoutes: { + '/': rootRouteRef, + }, + }); + + expect( + screen.queryByRole('menuitem', { name: 'Create template directory' }), + ).not.toBeInTheDocument(); + + await userEvent.click(screen.getByRole('button', { name: 'File' })); + + expect( + screen.getByRole('menuitem', { name: 'Create template directory' }), + ).toHaveAttribute('aria-disabled', 'true'); + }); + + it('should disable close editor by default', async () => { + await renderInTestApp(, { + mountedRoutes: { + '/': rootRouteRef, + }, + }); + + expect( + screen.queryByRole('menuitem', { name: 'Close template editor' }), + ).not.toBeInTheDocument(); + + await userEvent.click(screen.getByRole('button', { name: 'File' })); + + expect( + screen.getByRole('menuitem', { name: 'Close template editor' }), + ).toHaveAttribute('aria-disabled', 'true'); + }); + + it('should have an option to open the directory', async () => { + const onOpenDirectory = jest.fn(); + + await renderInTestApp( + , + { + mountedRoutes: { + '/': rootRouteRef, + }, + }, + ); + + expect( + screen.queryByRole('menuitem', { name: 'Open template directory' }), + ).not.toBeInTheDocument(); + + await userEvent.click(screen.getByRole('button', { name: 'File' })); + + await userEvent.click( + screen.getByRole('menuitem', { name: 'Open template directory' }), + ); + + expect(onOpenDirectory).toHaveBeenCalled(); + }); + + it('should have an option to create the directory', async () => { + const onCreateDirectory = jest.fn(); + + await renderInTestApp( + , + { + mountedRoutes: { + '/': rootRouteRef, + }, + }, + ); + + expect( + screen.queryByRole('menuitem', { name: 'Create template directory' }), + ).not.toBeInTheDocument(); + + await userEvent.click(screen.getByRole('button', { name: 'File' })); + + await userEvent.click( + screen.getByRole('menuitem', { name: 'Create template directory' }), + ); + + expect(onCreateDirectory).toHaveBeenCalled(); + }); + + it('should have an option to close the editor', async () => { + const onCloseDirectory = jest.fn(); + + await renderInTestApp( + , + { + mountedRoutes: { + '/': rootRouteRef, + }, + }, + ); + + expect( + screen.queryByRole('menuitem', { name: 'Close template editor' }), + ).not.toBeInTheDocument(); + + await userEvent.click(screen.getByRole('button', { name: 'File' })); + + await userEvent.click( + screen.getByRole('menuitem', { name: 'Close template editor' }), + ); + + expect(onCloseDirectory).toHaveBeenCalled(); + }); +}); diff --git a/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateEditorToolbarFileMenu.tsx b/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateEditorToolbarFileMenu.tsx index 9b8eee6162..f2bdfca6a2 100644 --- a/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateEditorToolbarFileMenu.tsx +++ b/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateEditorToolbarFileMenu.tsx @@ -15,16 +15,13 @@ */ import React, { MouseEvent, useCallback, useState } from 'react'; -import { useNavigate } from 'react-router-dom'; import Button from '@material-ui/core/Button'; import Menu from '@material-ui/core/Menu'; import MenuItem from '@material-ui/core/MenuItem'; -import { useRouteRef } from '@backstage/core-plugin-api'; import { useTranslationRef } from '@backstage/frontend-plugin-api'; -import { editRouteRef } from '../../../routes'; import { scaffolderTranslationRef } from '../../../translation'; export function TemplateEditorToolbarFileMenu(props: { @@ -33,8 +30,6 @@ export function TemplateEditorToolbarFileMenu(props: { onCloseDirectory?: () => void; }) { const { onOpenDirectory, onCreateDirectory, onCloseDirectory } = props; - const navigate = useNavigate(); - const editLink = useRouteRef(editRouteRef); const { t } = useTranslationRef(scaffolderTranslationRef); const [anchorEl, setAnchorEl] = useState(null); @@ -62,8 +57,7 @@ export function TemplateEditorToolbarFileMenu(props: { const handleCloseEditor = useCallback(() => { handleCloseMenu(); onCloseDirectory?.(); - navigate(editLink()); - }, [handleCloseMenu, onCloseDirectory, navigate, editLink]); + }, [handleCloseMenu, onCloseDirectory]); return ( <> @@ -88,7 +82,6 @@ export function TemplateEditorToolbarFileMenu(props: { vertical: 'top', horizontal: 'left', }} - keepMounted > {t('templateEditorToolbarFileMenu.options.openDirectory')} @@ -100,7 +93,7 @@ export function TemplateEditorToolbarFileMenu(props: { > {t('templateEditorToolbarFileMenu.options.createDirectory')} - + {t('templateEditorToolbarFileMenu.options.closeEditor')} diff --git a/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateFormPreviewer.tsx b/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateFormPreviewer.tsx index 160a6b4db9..b759dbc906 100644 --- a/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateFormPreviewer.tsx +++ b/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateFormPreviewer.tsx @@ -14,20 +14,26 @@ * limitations under the License. */ -import { alertApiRef, useApi } from '@backstage/core-plugin-api'; +import yaml from 'yaml'; +import React, { useCallback, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import useAsync from 'react-use/esm/useAsync'; + +import { makeStyles } from '@material-ui/core/styles'; + +import { alertApiRef, useApi, useRouteRef } from '@backstage/core-plugin-api'; import { catalogApiRef, humanizeEntityRef, } from '@backstage/plugin-catalog-react'; -import { makeStyles } from '@material-ui/core/styles'; -import React, { useCallback, useState } from 'react'; -import useAsync from 'react-use/esm/useAsync'; -import yaml from 'yaml'; import { LayoutOptions, FieldExtensionOptions, FormProps, } from '@backstage/plugin-scaffolder-react'; + +import { editRouteRef } from '../../../routes'; + import { TemplateEditorLayout, TemplateEditorLayoutToolbar, @@ -132,11 +138,18 @@ export const TemplateFormPreviewer = ({ const classes = useStyles(); const alertApi = useApi(alertApiRef); const catalogApi = useApi(catalogApiRef); + const navigate = useNavigate(); + const editLink = useRouteRef(editRouteRef); + const [errorText, setErrorText] = useState(); const [selectedTemplate, setSelectedTemplate] = useState(); const [templateOptions, setTemplateOptions] = useState([]); const [templateYaml, setTemplateYaml] = useState(defaultPreviewTemplate); + const handleCloseDirectory = useCallback(() => { + navigate(editLink()); + }, [navigate, editLink]); + useAsync( () => catalogApi @@ -184,7 +197,9 @@ export const TemplateFormPreviewer = ({ - + { expect(result.current.directory).toBeUndefined(); await act(async () => { - result.current.handleOpenDirectory(); + result.current.openDirectory(); }); expect(requestDirectoryAccess).toHaveBeenCalled(); @@ -103,7 +103,7 @@ describe('useTemplateDirectory', () => { const { result } = renderHook(() => useTemplateDirectory()); await act(async () => { - result.current.handleCreateDirectory(); + result.current.createDirectory(); }); expect(requestDirectoryAccess).toHaveBeenCalled(); @@ -124,7 +124,7 @@ describe('useTemplateDirectory', () => { expect(result.current.directory).toBeUndefined(); await act(async () => { - result.current.handleCloseDirectory(); + result.current.closeDirectory(); }); expect(setDirectory).toHaveBeenCalledWith(undefined); diff --git a/plugins/scaffolder/src/alpha/components/TemplateEditorPage/useTemplateDirectory.ts b/plugins/scaffolder/src/alpha/components/TemplateEditorPage/useTemplateDirectory.ts index 47b05db872..eefb37568d 100644 --- a/plugins/scaffolder/src/alpha/components/TemplateEditorPage/useTemplateDirectory.ts +++ b/plugins/scaffolder/src/alpha/components/TemplateEditorPage/useTemplateDirectory.ts @@ -28,9 +28,9 @@ export function useTemplateDirectory(): { directory?: WebDirectoryAccess; loading: boolean; error?: Error; - handleOpenDirectory: () => void; - handleCreateDirectory: () => void; - handleCloseDirectory: () => void; + openDirectory: () => Promise; + createDirectory: () => Promise; + closeDirectory: () => Promise; } { const { value, loading, error, retry } = useAsyncRetry(async () => { const directory = await WebFileSystemStore.getDirectory(); @@ -38,29 +38,29 @@ export function useTemplateDirectory(): { return WebFileSystemAccess.fromHandle(directory); }, []); - const handleOpenDirectory = useCallback(() => { - WebFileSystemAccess.requestDirectoryAccess() + const openDirectory = useCallback(() => { + return WebFileSystemAccess.requestDirectoryAccess() .then(WebFileSystemStore.setDirectory) .then(retry); }, [retry]); - const handleCreateDirectory = useCallback(() => { - WebFileSystemAccess.requestDirectoryAccess() + const createDirectory = useCallback(() => { + return WebFileSystemAccess.requestDirectoryAccess() .then(createExampleTemplate) .then(WebFileSystemStore.setDirectory) .then(retry); }, [retry]); - const handleCloseDirectory = useCallback(() => { - WebFileSystemStore.setDirectory(undefined).then(retry); + const closeDirectory = useCallback(() => { + return WebFileSystemStore.setDirectory(undefined).then(retry); }, [retry]); return { directory: value, loading, error, - handleOpenDirectory, - handleCreateDirectory, - handleCloseDirectory, + openDirectory, + createDirectory, + closeDirectory, }; }