From a741deca432bbeb99473ad7d93384d5909b260f5 Mon Sep 17 00:00:00 2001 From: Alyssa Wang Date: Wed, 1 May 2024 13:35:42 -0400 Subject: [PATCH] FI-2608: Update ctrl + enter run test hotkey (#483) * refactor input types button toggle * refactor input components * add lock on ctrl key for mac * remove redundant input props * update tests * allow both ctrl and meta keybindings irrespective of OS * npm audit fix --------- Co-authored-by: Alyssa Wang --- .../components/InputsModal/InputFields.tsx | 66 +++++++ .../components/InputsModal/InputsModal.tsx | 172 +++++------------- .../__tests__/InputsModal.test.tsx | 35 ++-- client/src/components/InputsModal/styles.tsx | 13 +- .../src/components/TestSuite/TestSession.tsx | 26 ++- .../TestListItem/TestListItem.tsx | 2 +- package-lock.json | 6 +- 7 files changed, 156 insertions(+), 164 deletions(-) create mode 100644 client/src/components/InputsModal/InputFields.tsx diff --git a/client/src/components/InputsModal/InputFields.tsx b/client/src/components/InputsModal/InputFields.tsx new file mode 100644 index 000000000..15f95f703 --- /dev/null +++ b/client/src/components/InputsModal/InputFields.tsx @@ -0,0 +1,66 @@ +import React, { FC } from 'react'; +import { List } from '@mui/material'; +import { TestInput } from '~/models/testSuiteModels'; +import InputOAuthCredentials from '~/components/InputsModal/InputOAuthCredentials'; +import InputCheckboxGroup from '~/components/InputsModal/InputCheckboxGroup'; +import InputRadioGroup from '~/components/InputsModal/InputRadioGroup'; +import InputTextField from '~/components/InputsModal/InputTextField'; + +export interface InputFieldsProps { + inputs: TestInput[]; + inputsMap: Map; + setInputsMap: (newInputsMap: Map, editStatus?: boolean) => void; +} + +const InputFields: FC = ({ inputs, inputsMap, setInputsMap }) => { + return ( + + {inputs.map((requirement: TestInput, index: number) => { + switch (requirement.type) { + case 'oauth_credentials': + return ( + setInputsMap(newInputsMap)} + key={`input-${index}`} + /> + ); + case 'checkbox': + return ( + setInputsMap(newInputsMap, editStatus)} + key={`input-${index}`} + /> + ); + case 'radio': + return ( + setInputsMap(newInputsMap)} + key={`input-${index}`} + /> + ); + default: + return ( + setInputsMap(newInputsMap)} + key={`input-${index}`} + /> + ); + } + })} + + ); +}; + +export default InputFields; diff --git a/client/src/components/InputsModal/InputsModal.tsx b/client/src/components/InputsModal/InputsModal.tsx index 2c4661aa9..1009d3d3a 100644 --- a/client/src/components/InputsModal/InputsModal.tsx +++ b/client/src/components/InputsModal/InputsModal.tsx @@ -6,7 +6,6 @@ import { DialogContent, DialogContentText, DialogTitle, - List, TextField, ToggleButtonGroup, ToggleButton, @@ -20,29 +19,25 @@ import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import YAML from 'js-yaml'; import { useSnackbar } from 'notistack'; -import { OAuthCredentials, RunnableType, TestInput } from '~/models/testSuiteModels'; -import InputOAuthCredentials from './InputOAuthCredentials'; -import InputCheckboxGroup from './InputCheckboxGroup'; -import InputRadioGroup from './InputRadioGroup'; -import InputTextField from './InputTextField'; -import CustomTooltip from '../_common/CustomTooltip'; -import useStyles from './styles'; +import { OAuthCredentials, Runnable, RunnableType, TestInput } from '~/models/testSuiteModels'; +import CustomTooltip from '~/components/_common/CustomTooltip'; +import InputFields from '~/components/InputsModal/InputFields'; +import useStyles from '~/components/InputsModal/styles'; import DownloadFileButton from '../_common/DownloadFileButton'; import UploadFileButton from '../_common/UploadFileButton'; import CopyButton from '../_common/CopyButton'; export interface InputsModalProps { + modalVisible: boolean; + hideModal: () => void; + runnable: Runnable; runnableType: RunnableType; - runnableId: string; - title: string; - inputInstructions?: string; inputs: TestInput[]; - hideModal: () => void; - createTestRun: (runnableType: RunnableType, runnableId: string, inputs: TestInput[]) => void; sessionData: Map; + createTestRun: (runnableType: RunnableType, runnableId: string, inputs: TestInput[]) => void; } -function runnableTypeReadable(runnableType: RunnableType) { +const runnableTypeReadable = (runnableType: RunnableType) => { switch (runnableType) { case RunnableType.TestSuite: return 'test suite'; @@ -51,21 +46,19 @@ function runnableTypeReadable(runnableType: RunnableType) { case RunnableType.Test: return 'test'; } -} +}; const InputsModal: FC = ({ + modalVisible, + hideModal, + runnable, runnableType, - runnableId, - title, - inputInstructions, inputs, - hideModal, - createTestRun, sessionData, + createTestRun, }) => { const { classes } = useStyles(); const { enqueueSnackbar } = useSnackbar(); - const [open, setOpen] = React.useState(true); const [inputsEdited, setInputsEdited] = React.useState(false); const [inputsMap, setInputsMap] = React.useState>(new Map()); const [inputType, setInputType] = React.useState('Field'); @@ -117,55 +110,14 @@ const InputsModal: FC = ({ }); const instructions = - inputInstructions || - `Please fill out required fields in order to run the ${runnableTypeReadable(runnableType)}.`; - - const inputFields = inputs.map((requirement: TestInput, index: number) => { - switch (requirement.type) { - case 'oauth_credentials': - return ( - handleSetInputsMap(newInputsMap)} - key={`input-${index}`} - /> - ); - case 'checkbox': - return ( - - handleSetInputsMap(newInputsMap, editStatus) - } - key={`input-${index}`} - /> - ); - case 'radio': - return ( - handleSetInputsMap(newInputsMap)} - key={`input-${index}`} - /> - ); - default: - return ( - handleSetInputsMap(newInputsMap)} - key={`input-${index}`} - /> - ); - } - }); + runnable.input_instructions || + `Please fill out required fields in order to run the ${runnableTypeReadable(runnableType)}.` + + (inputType === 'Field' + ? '' + : ' In this view, only changes to the value attribute of an element will be saved. \ + Further, only elements with names that match an input defined for the current suite, \ + group, or test will be saved. The intended use of this view is to provide a template \ + for users to copy/paste in order to avoid filling out individual fields every time.'); useEffect(() => { inputsMap.clear(); @@ -194,7 +146,7 @@ const InputsModal: FC = ({ setFileType('txt'); break; } - }, [inputType, open]); + }, [inputType, modalVisible]); const handleInputTypeChange = (e: React.MouseEvent, value: string) => { if (value !== null) setInputType(value); @@ -211,13 +163,8 @@ const InputsModal: FC = ({ }; const handleSubmitKeydown = (e: React.KeyboardEvent) => { - if ( - open && - e.key === 'Enter' && - (e.metaKey || e.ctrlKey) && - !missingRequiredInput && - !invalidInput - ) { + const opKey = e.metaKey || e.ctrlKey; + if (modalVisible && e.key === 'Enter' && opKey && !missingRequiredInput && !invalidInput) { submitClicked(); } }; @@ -227,7 +174,7 @@ const InputsModal: FC = ({ inputsMap.forEach((input_value, input_name) => { inputs_with_values.push({ name: input_name, value: input_value, type: 'text' }); }); - createTestRun(runnableType, runnableId, inputs_with_values); + createTestRun(runnableType, runnable.id, inputs_with_values); closeModal(); }; @@ -269,11 +216,7 @@ const InputsModal: FC = ({ const parseSerialChanges = (changes: string): TestInput[] | undefined => { let parsed: TestInput[]; try { - if (inputType === 'JSON') { - parsed = JSON.parse(changes) as TestInput[]; - } else { - parsed = YAML.load(changes) as TestInput[]; - } + parsed = (inputType === 'JSON' ? JSON.parse(changes) : YAML.load(changes)) as TestInput[]; // Convert OAuth input values to strings; parsed needs to be an array parsed.forEach((input) => { if (input.type === 'oauth_credentials') { @@ -304,14 +247,13 @@ const InputsModal: FC = ({ const closeModal = (edited = false) => { // For external clicks, check if inputs have been edited first if (!edited) { - setOpen(false); hideModal(); } }; return ( = ({ - {title} + {runnable.title} closeModal()} aria-label="cancel" - sx={{ - position: 'absolute', - right: 8, - top: 8, - }} + className={classes.cancelButton} > @@ -339,20 +277,15 @@ const InputsModal: FC = ({
- - - {instructions + - (inputType === 'Field' - ? '' - : ' In this view, only changes to the value attribute of an element will be saved. Further, only elements with names that match an input defined for the current suite, group, or test will be saved. The intended use of this view is to provide a template for users to copy/paste in order to avoid filling out individual fields every time.')} - + + {instructions} {inputType === 'Field' ? ( - {inputFields} + ) : ( - + = ({ onChange={handleInputTypeChange} className={classes.toggleButtonGroup} > - - Field - - - JSON - - - YAML - + {['Field', 'JSON', 'YAML'].map((type) => { + return ( + + {type} + + ); + })} diff --git a/client/src/components/InputsModal/__tests__/InputsModal.test.tsx b/client/src/components/InputsModal/__tests__/InputsModal.test.tsx index e9efecb16..1258bec40 100644 --- a/client/src/components/InputsModal/__tests__/InputsModal.test.tsx +++ b/client/src/components/InputsModal/__tests__/InputsModal.test.tsx @@ -7,6 +7,7 @@ import ThemeProvider from 'components/ThemeProvider'; import { SnackbarProvider } from 'notistack'; import { vi } from 'vitest'; +import { mockedTestGroup } from '~/components/_common/__mocked_data__/mockData'; const hideModalMock = vi.fn(); const createTestRunMock = vi.fn(); @@ -44,19 +45,19 @@ test('Modal visible and inputs are shown', () => { ); - const titleText = screen.getByText('Modal Title'); + const titleText = screen.getByText('Mock Test Group'); expect(titleText).toBeVisible(); testInputs.forEach((input: TestInput) => { @@ -75,13 +76,13 @@ test('Pressing cancel hides the modal', () => { @@ -97,13 +98,13 @@ test('Pressing submit hides the modal', () => { @@ -119,13 +120,13 @@ test('Field Inputs shown in JSON and YAML', () => { @@ -154,13 +155,13 @@ test('Values in Field Inputs shown in JSON and YAML', () => { diff --git a/client/src/components/InputsModal/styles.tsx b/client/src/components/InputsModal/styles.tsx index 44855cab2..4812c7f41 100644 --- a/client/src/components/InputsModal/styles.tsx +++ b/client/src/components/InputsModal/styles.tsx @@ -2,10 +2,10 @@ import { Theme } from '@mui/material/styles'; import { makeStyles } from 'tss-react/mui'; export default makeStyles()((theme: Theme) => ({ - textarea: { - resize: 'vertical', - maxHeight: '400px', - overflow: 'auto !important', + cancelButton: { + position: 'absolute', + right: 8, + top: 8, }, inputField: { marginTop: '0 !important', @@ -40,6 +40,11 @@ export default makeStyles()((theme: Theme) => ({ marginLeft: '5px', verticalAlign: 'text-bottom', }, + textarea: { + resize: 'vertical', + maxHeight: '400px', + overflow: 'auto !important', + }, oauthCard: { width: '100%', mx: 2, diff --git a/client/src/components/TestSuite/TestSession.tsx b/client/src/components/TestSuite/TestSession.tsx index 3f0bef6ef..dcfd36aaa 100644 --- a/client/src/components/TestSuite/TestSession.tsx +++ b/client/src/components/TestSuite/TestSession.tsx @@ -172,7 +172,7 @@ const TestSessionComponent: FC = ({ }; }); - const showInputsModal = (runnableType: RunnableType, runnableId: string, inputs: TestInput[]) => { + const showInputsModal = (runnableType: RunnableType, inputs: TestInput[]) => { setInputs(inputs); setRunnableType(runnableType); setInputModalVisible(true); @@ -254,7 +254,7 @@ const TestSessionComponent: FC = ({ input.value = sessionData.get(input.name); }); if (runnable?.inputs && runnable.inputs.length > 0) { - showInputsModal(runnableType, runnableId, runnable.inputs); + showInputsModal(runnableType, runnable.inputs); } else { createTestRun(runnableType, runnableId, []); } @@ -271,6 +271,7 @@ const TestSessionComponent: FC = ({ const runnable = runnableMap.get(runnableId); if (runnable) setIsRunning(runnable, true); setCurrentRunnables({ ...currentRunnables, [testSession.id]: runnableId }); + setInputModalVisible(false); setTestRun(testRun); setTestRunId(testRun.id); setTestRunCancelled(false); @@ -403,18 +404,15 @@ const TestSessionComponent: FC = ({ > {renderView(view || 'run')} - {inputModalVisible && ( - setInputModalVisible(false)} - /> - )} + setInputModalVisible(false)} + runnable={runnableMap.get(selectedRunnable) as Runnable} + runnableType={runnableType} + inputs={inputs} + sessionData={sessionData} + createTestRun={createTestRun} + /> = ({ className={classes.accordion} sx={view === 'report' ? { pointerEvents: 'none' } : {}} expanded={open} - TransitionProps={{ unmountOnExit: true }} + slotProps={{ transition: { unmountOnExit: true } }} onClick={handleAccordionClick} onKeyDown={(e) => { if (e.key === 'Enter') { diff --git a/package-lock.json b/package-lock.json index 4a410d0e6..98fe7795f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9776,9 +9776,9 @@ "dev": true }, "node_modules/ejs": { - "version": "3.1.9", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.9.tgz", - "integrity": "sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==", + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", "dev": true, "dependencies": { "jake": "^10.8.5"