test(scaffolder): editor toolbar files menu

Signed-off-by: Camila Belo <camilaibs@gmail.com>
This commit is contained in:
Camila Belo
2024-10-04 09:25:45 +02:00
parent ad8ff14dd0
commit c18d9259ae
8 changed files with 218 additions and 40 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder': patch
---
Add tests for the `TemplateEditorToolbarFilesMenu` component.
+1 -1
View File
@@ -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".
@@ -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<string>();
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 (
<DirectoryEditorProvider directory={directory}>
<DryRunProvider>
@@ -68,13 +80,13 @@ export const TemplateEditor = (props: {
<TemplateEditorToolbar fieldExtensions={fieldExtensions}>
<TemplateEditorToolbarFileMenu
onOpenDirectory={handleOpenDirectory}
onCloseDirectory={handleCloseDirectory}
onCreateDirectory={handleCreateDirectory}
onCloseDirectory={handleCloseDirectory}
/>
</TemplateEditorToolbar>
</TemplateEditorLayoutToolbar>
<TemplateEditorLayoutBrowser>
<TemplateEditorBrowser onClose={handleCloseDirectory} />
<TemplateEditorBrowser onClose={closeDirectory} />
</TemplateEditorLayoutBrowser>
<TemplateEditorLayoutFiles>
<TemplateEditorTextArea.DirectoryEditor errorText={errorText} />
@@ -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(<TemplateEditorToolbarFileMenu />, {
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(<TemplateEditorToolbarFileMenu />, {
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(<TemplateEditorToolbarFileMenu />, {
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(
<TemplateEditorToolbarFileMenu onOpenDirectory={onOpenDirectory} />,
{
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(
<TemplateEditorToolbarFileMenu onCreateDirectory={onCreateDirectory} />,
{
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(
<TemplateEditorToolbarFileMenu onCloseDirectory={onCloseDirectory} />,
{
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();
});
});
@@ -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 | HTMLElement>(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
>
<MenuItem onClick={handleOpenDirectory} disabled={!onOpenDirectory}>
{t('templateEditorToolbarFileMenu.options.openDirectory')}
@@ -100,7 +93,7 @@ export function TemplateEditorToolbarFileMenu(props: {
>
{t('templateEditorToolbarFileMenu.options.createDirectory')}
</MenuItem>
<MenuItem onClick={handleCloseEditor}>
<MenuItem onClick={handleCloseEditor} disabled={!onCloseDirectory}>
{t('templateEditorToolbarFileMenu.options.closeEditor')}
</MenuItem>
</Menu>
@@ -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<string>();
const [selectedTemplate, setSelectedTemplate] = useState<TemplateOption>();
const [templateOptions, setTemplateOptions] = useState<TemplateOption[]>([]);
const [templateYaml, setTemplateYaml] = useState(defaultPreviewTemplate);
const handleCloseDirectory = useCallback(() => {
navigate(editLink());
}, [navigate, editLink]);
useAsync(
() =>
catalogApi
@@ -184,7 +197,9 @@ export const TemplateFormPreviewer = ({
<TemplateEditorLayout classes={{ root: classes.root }}>
<TemplateEditorLayoutToolbar>
<TemplateEditorToolbar fieldExtensions={customFieldExtensions}>
<TemplateEditorToolbarFileMenu />
<TemplateEditorToolbarFileMenu
onCloseDirectory={handleCloseDirectory}
/>
<TemplateEditorToolbarTemplatesMenu
options={templateOptions}
selectedOption={selectedTemplate}
@@ -78,7 +78,7 @@ describe('useTemplateDirectory', () => {
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);
@@ -28,9 +28,9 @@ export function useTemplateDirectory(): {
directory?: WebDirectoryAccess;
loading: boolean;
error?: Error;
handleOpenDirectory: () => void;
handleCreateDirectory: () => void;
handleCloseDirectory: () => void;
openDirectory: () => Promise<void>;
createDirectory: () => Promise<void>;
closeDirectory: () => Promise<void>;
} {
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,
};
}