diff --git a/CHANGELOG.md b/CHANGELOG.md index c0df4663d..e1cf3c5bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,28 @@ +# 0.4.20 +* FI-2077: Improve colors and contrasts for a11y by @AlyssaWang in + https://github.com/inferno-framework/inferno-core/pull/392 +* Revert "FI-2035: Improve error handling for validator errors (#379)" by + @Jammjammjamm in https://github.com/inferno-framework/inferno-core/pull/393 + +# 0.4.19 +* FI-2053: Fix inputs dialog overflow by @AlyssaWang in + https://github.com/inferno-framework/inferno-core/pull/382 +* FI-2038: Prevent modal close on edit by @AlyssaWang in + https://github.com/inferno-framework/inferno-core/pull/383 +* FI-2094: Improve tooltip a11y by @AlyssaWang in + https://github.com/inferno-framework/inferno-core/pull/386 +* FI-2035: Improve error handling for validator errors by @dehall in + https://github.com/inferno-framework/inferno-core/pull/379 +* FI-2070: Inferno Framework Documentation Advanced Test Features Information + Fix by @emichaud998 in + https://github.com/inferno-framework/inferno-core/pull/385 +* FI-2041: Custom suites with no ids now throw standard error by @alisawallace + in https://github.com/inferno-framework/inferno-core/pull/387 +* FI-2156: Dependabot updates by @Jammjammjamm in + https://github.com/inferno-framework/inferno-core/pull/390 +* FI-2086: fix errors on webpack shutdown by @alisawallace in + https://github.com/inferno-framework/inferno-core/pull/389 + # 0.4.18 * Fix a bug which could prevent some test results from appearing until the page is reloaded. diff --git a/Gemfile.lock b/Gemfile.lock index 0b15f620d..ef7f229fc 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,8 +1,8 @@ PATH remote: . specs: - inferno_core (0.4.18) - activesupport (~> 6.1) + inferno_core (0.4.20) + activesupport (~> 6.1.7.5) base62-rb (= 0.3.1) blueprinter (= 0.25.2) dotenv (~> 2.7) @@ -19,7 +19,7 @@ PATH oj (= 3.11.0) pry pry-byebug - puma (~> 5.3) + puma (~> 5.6.7) rake (~> 13.0) sequel (~> 5.42.0) sidekiq (~> 6.5.6) @@ -30,7 +30,7 @@ PATH GEM remote: https://rubygems.org/ specs: - activesupport (6.1.7.6) + activesupport (6.1.7.3) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) @@ -220,11 +220,9 @@ GEM method_source (~> 1.0) pry-byebug (3.10.1) byebug (~> 11.0) - pry (>= 0.13, < 0.15) - psych (5.1.0) - stringio - public_suffix (5.0.3) - puma (5.6.7) + pry (~> 0.13.0) + public_suffix (4.0.7) + puma (5.6.6) nio4r (~> 2.0) racc (1.7.1) rack (2.2.8) @@ -299,11 +297,10 @@ GEM rexml simplecov (~> 0.19) simplecov-html (0.12.3) - simplecov_json_formatter (0.1.4) - sqlite3 (1.6.6-arm64-darwin) - sqlite3 (1.6.6-x86_64-darwin) - sqlite3 (1.6.6-x86_64-linux) - stringio (3.0.8) + simplecov_json_formatter (0.1.3) + sqlite3 (1.6.3-arm64-darwin) + sqlite3 (1.6.3-x86_64-darwin) + sqlite3 (1.6.3-x86_64-linux) strings (0.2.1) strings-ansi (~> 0.2) unicode-display_width (>= 1.5, < 3.0) @@ -342,6 +339,7 @@ PLATFORMS arm64-darwin-21 arm64-darwin-22 x86_64-darwin-20 + x86_64-darwin-22 x86_64-linux DEPENDENCIES diff --git a/Procfile b/Procfile index 5f35ca5da..4433c1748 100644 --- a/Procfile +++ b/Procfile @@ -1,3 +1,3 @@ web: bundle exec puma worker: bundle exec sidekiq -r ./worker.rb -webpack: npm run start +webpack: ./node_modules/.bin/webpack serve --config ./webpack.config.js --mode=development diff --git a/client/src/components/App/__tests__/App.test.tsx b/client/src/components/App/__tests__/App.test.tsx index 96aec5df7..8205bc0af 100644 --- a/client/src/components/App/__tests__/App.test.tsx +++ b/client/src/components/App/__tests__/App.test.tsx @@ -1,5 +1,7 @@ import React from 'react'; +import { act } from 'react-dom/test-utils'; import { render } from '@testing-library/react'; + import { vi } from 'vitest'; import { SnackbarProvider } from 'notistack'; @@ -21,16 +23,17 @@ describe('The App Root Component', () => { vi.clearAllMocks(); }); - it('sets Test Suite state on mount', () => { + it('sets Test Suite state on mount', async () => { const getTestSuites = vi.spyOn(testSuitesApi, 'getTestSuites'); getTestSuites.mockResolvedValue(testSuites); - - render( - - - - - + await act(() => + render( + + + + + + ) ); expect(getTestSuites).toBeCalledTimes(1); diff --git a/client/src/components/InputsModal/FieldLabel.tsx b/client/src/components/InputsModal/FieldLabel.tsx index 14439b272..1fd1084ea 100644 --- a/client/src/components/InputsModal/FieldLabel.tsx +++ b/client/src/components/InputsModal/FieldLabel.tsx @@ -2,12 +2,14 @@ import React, { FC } from 'react'; import LockIcon from '@mui/icons-material/Lock'; import { TestInput } from '~/models/testSuiteModels'; import useStyles from './styles'; +import RequiredInputWarning from './RequiredInputWarning'; export interface FieldLabelProps { requirement: TestInput; + isMissingInput?: boolean; } -const FieldLabel: FC = ({ requirement }) => { +const FieldLabel: FC = ({ requirement, isMissingInput = false }) => { const { classes } = useStyles(); const fieldLabelText = requirement.title || requirement.name; @@ -20,6 +22,7 @@ const FieldLabel: FC = ({ requirement }) => { return ( <> + {isMissingInput && } {fieldLabelText} {requiredLabel} {lockedIcon} diff --git a/client/src/components/InputsModal/InputCheckboxGroup.tsx b/client/src/components/InputsModal/InputCheckboxGroup.tsx index de65941f1..725b7142f 100644 --- a/client/src/components/InputsModal/InputCheckboxGroup.tsx +++ b/client/src/components/InputsModal/InputCheckboxGroup.tsx @@ -26,6 +26,7 @@ const InputCheckboxGroup: FC = ({ setInputsMap, }) => { const { classes } = useStyles(); + const [hasBeenModified, setHasBeenModified] = React.useState(false); const [values, setValues] = React.useState(() => { // Default values should be in form ['value'] where all values are checked @@ -61,6 +62,9 @@ const InputCheckboxGroup: FC = ({ return startingValues as CheckboxValues; }); + const isMissingInput = + hasBeenModified && !requirement.optional && inputsMap.get(requirement.name) === '[]'; + useEffect(() => { // Make sure starting values get set in inputsMap inputsMap.set(requirement.name, transformValuesToJSONArray(values)); @@ -75,6 +79,7 @@ const InputCheckboxGroup: FC = ({ inputsMap.set(requirement.name, transformValuesToJSONArray(newValues)); setInputsMap(new Map(inputsMap)); setValues(newValues); + setHasBeenModified(true); }; // Convert map from item name to checked status back to array of checked values @@ -93,11 +98,13 @@ const InputCheckboxGroup: FC = ({ component="fieldset" id={`requirement${index}_input`} disabled={requirement.locked} + required={!requirement.optional} + error={isMissingInput} fullWidth className={classes.inputField} > - - + + {requirement.description && ( {requirement.description} @@ -108,8 +115,14 @@ const InputCheckboxGroup: FC = ({ control={ { + if (e.currentTarget === e.target) { + setHasBeenModified(true); + } + }} onChange={handleChange} /> } diff --git a/client/src/components/InputsModal/InputOAuthCredentials.tsx b/client/src/components/InputsModal/InputOAuthCredentials.tsx index 66c26a67a..adfa2796d 100644 --- a/client/src/components/InputsModal/InputOAuthCredentials.tsx +++ b/client/src/components/InputsModal/InputOAuthCredentials.tsx @@ -12,6 +12,7 @@ import { import { OAuthCredentials, TestInput } from '~/models/testSuiteModels'; import FieldLabel from './FieldLabel'; import useStyles from './styles'; +import RequiredInputWarning from './RequiredInputWarning'; export interface InputOAuthCredentialsProps { requirement: TestInput; @@ -35,22 +36,20 @@ const InputOAuthCredentials: FC = ({ setInputsMap, }) => { const { classes } = useStyles(); + const [hasBeenModified, setHasBeenModified] = React.useState({}); // Convert OAuth string to Object // OAuth should be an Object while in this component but should be converted to a string // before being updated in the inputs map - const oAuthCredentials = ( - inputsMap.get(requirement.name) - ? JSON.parse(inputsMap.get(requirement.name) as string) - : { - access_token: '', - refresh_token: '', - expires_in: '', - client_id: '', - client_secret: '', - token_url: '', - } - ) as OAuthCredentials; + const oAuthCredentials = { + access_token: '', + refresh_token: '', + expires_in: '', + client_id: '', + client_secret: '', + token_url: '', + ...JSON.parse((inputsMap.get(requirement.name) as string) || '{}'), + } as OAuthCredentials; const showRefreshDetails = !!oAuthCredentials.refresh_token; @@ -91,23 +90,45 @@ const InputOAuthCredentials: FC = ({ }, ]; + const getIsMissingInput = (field: InputOAuthField) => { + return ( + hasBeenModified[field.name as keyof typeof hasBeenModified] && + field.required && + !oAuthCredentials[field.name as keyof OAuthCredentials] + ); + }; + const oAuthField = (field: InputOAuthField) => { - const fieldLabel = field.required + const fieldName = field.required ? `${(field.label || field.name) as string} (required)` : field.label || field.name; + + const fieldLabel = ( + <> + {getIsMissingInput(field) && } + {fieldName} + + ); + return ( { + if (e.currentTarget === e.target) { + setHasBeenModified({ ...hasBeenModified, [field.name]: true }); + } + }} onChange={(event) => { const value = event.target.value; oAuthCredentials[field.name as keyof OAuthCredentials] = value; @@ -128,7 +149,6 @@ const InputOAuthCredentials: FC = ({ required={!requirement.optional} disabled={requirement.locked} className={classes.inputLabel} - shrink > diff --git a/client/src/components/InputsModal/InputRadioGroup.tsx b/client/src/components/InputsModal/InputRadioGroup.tsx index 929826e5d..a784b38d9 100644 --- a/client/src/components/InputsModal/InputRadioGroup.tsx +++ b/client/src/components/InputsModal/InputRadioGroup.tsx @@ -66,7 +66,7 @@ const InputRadioGroup: FC = ({ {requirement.options?.list_options?.map((option, i) => ( } + control={} label={option.label} key={`radio-button-${i}`} /> diff --git a/client/src/components/InputsModal/InputTextArea.tsx b/client/src/components/InputsModal/InputTextArea.tsx index 4ebd01074..68db3194e 100644 --- a/client/src/components/InputsModal/InputTextArea.tsx +++ b/client/src/components/InputsModal/InputTextArea.tsx @@ -16,26 +16,34 @@ const InputTextArea: FC = ({ requirement, index, inputsMap, const { classes } = useStyles(); const [hasBeenModified, setHasBeenModified] = React.useState(false); + const isMissingInput = + hasBeenModified && !requirement.optional && !inputsMap.get(requirement.name); + return ( } + label={} helperText={requirement.description} value={inputsMap.get(requirement.name)} multiline rows={4} + onBlur={(e) => { + if (e.currentTarget === e.target) { + setHasBeenModified(true); + } + }} onChange={(event) => { const value = event.target.value; inputsMap.set(requirement.name, value); setInputsMap(new Map(inputsMap)); - setHasBeenModified(true); }} FormHelperTextProps={{ sx: { '&.Mui-disabled': { color: lightTheme.palette.common.grayDark } }, diff --git a/client/src/components/InputsModal/InputTextField.tsx b/client/src/components/InputsModal/InputTextField.tsx index 2374b8feb..e54298be4 100644 --- a/client/src/components/InputsModal/InputTextField.tsx +++ b/client/src/components/InputsModal/InputTextField.tsx @@ -21,24 +21,32 @@ const InputTextField: FC = ({ const { classes } = useStyles(); const [hasBeenModified, setHasBeenModified] = React.useState(false); + const isMissingInput = + hasBeenModified && !requirement.optional && !inputsMap.get(requirement.name); + return ( } + label={} helperText={requirement.description} value={inputsMap.get(requirement.name)} + onBlur={(e) => { + if (e.currentTarget === e.target) { + setHasBeenModified(true); + } + }} onChange={(event) => { const value = event.target.value; inputsMap.set(requirement.name, value); setInputsMap(new Map(inputsMap)); - setHasBeenModified(true); }} InputLabelProps={{ shrink: true }} FormHelperTextProps={{ diff --git a/client/src/components/InputsModal/InputsModal.tsx b/client/src/components/InputsModal/InputsModal.tsx index ad65e0764..d78cf5be0 100644 --- a/client/src/components/InputsModal/InputsModal.tsx +++ b/client/src/components/InputsModal/InputsModal.tsx @@ -98,8 +98,7 @@ const InputsModal: FC = ({ ) as OAuthCredentials; const accessTokenIsEmpty = oAuthJSON.access_token === ''; const refreshIsEmpty = - oAuthJSON.refresh_token !== '' && - (oAuthJSON.token_url === '' || oAuthJSON.client_id === ''); + oAuthJSON.refresh_token !== '' && (!oAuthJSON.token_url || !oAuthJSON.client_id); oAuthMissingRequiredInput = (accessTokenIsEmpty && !input.optional) || refreshIsEmpty; } catch (e: unknown) { const errorMessage = e instanceof Error ? e.message : String(e); @@ -281,8 +280,9 @@ const InputsModal: FC = ({ const parsedChanges = parseSerialChanges(serialChanges); if (parsedChanges !== undefined && parsedChanges.keys !== undefined) { parsedChanges.forEach((change: TestInput) => { - if (!change.locked && change.value !== undefined) + if (!change.locked && change.value !== undefined) { inputsMap.set(change.name, change.value || ''); + } }); } handleSetInputsMap(new Map(inputsMap), true); @@ -349,6 +349,7 @@ const InputsModal: FC = ({ input: classes.serialInput, }, }} + color="secondary" fullWidth multiline data-testid="serial-input" @@ -399,7 +400,7 @@ const InputsModal: FC = ({ color="secondary" data-testid="cancel-button" onClick={() => closeModal()} - sx={{ mr: 1 }} + sx={{ mr: 1, fontWeight: 'bold' }} > Cancel @@ -409,6 +410,7 @@ const InputsModal: FC = ({ disableElevation onClick={submitClicked} disabled={missingRequiredInput || invalidInput} + sx={{ fontWeight: 'bold' }} > Submit diff --git a/client/src/components/InputsModal/RequiredInputWarning.tsx b/client/src/components/InputsModal/RequiredInputWarning.tsx index d0855a55f..51bef2437 100644 --- a/client/src/components/InputsModal/RequiredInputWarning.tsx +++ b/client/src/components/InputsModal/RequiredInputWarning.tsx @@ -1,11 +1,19 @@ import React, { FC } from 'react'; -import WarningIcon from '@mui/icons-material/Warning'; +import { Report } from '@mui/icons-material'; import CustomTooltip from '~/components/_common/CustomTooltip'; const RequiredInputWarning: FC = () => { return ( - + ); }; diff --git a/client/src/components/InputsModal/styles.tsx b/client/src/components/InputsModal/styles.tsx index c6327a761..729e6b272 100644 --- a/client/src/components/InputsModal/styles.tsx +++ b/client/src/components/InputsModal/styles.tsx @@ -13,7 +13,7 @@ export default makeStyles()((theme: Theme) => ({ color: theme.palette.common.grayDarkest, }, '& label.Mui-focused': { - color: theme.palette.common.orangeDarkest, + color: theme.palette.secondary.main, }, '& label.Mui-disabled': { color: theme.palette.common.gray, @@ -23,9 +23,8 @@ export default makeStyles()((theme: Theme) => ({ }, }, inputLabel: { - color: theme.palette.common.grayDarkest, + color: theme.palette.common.grayDarker, fontWeight: 600, - fontSize: '.75rem', }, lockedIcon: { marginLeft: '5px', @@ -36,7 +35,7 @@ export default makeStyles()((theme: Theme) => ({ margin: '8px 0', borderColor: theme.palette.common.grayLight, '&:focus-within': { - borderColor: theme.palette.primary.main, + borderColor: theme.palette.secondary.main, }, }, serialInput: { diff --git a/client/src/components/PresetsSelector/PresetsSelector.tsx b/client/src/components/PresetsSelector/PresetsSelector.tsx index 37645f195..94672b5ba 100644 --- a/client/src/components/PresetsSelector/PresetsSelector.tsx +++ b/client/src/components/PresetsSelector/PresetsSelector.tsx @@ -109,7 +109,7 @@ const PresetsSelector: FC = ({ presets, testSessionId, getSes diff --git a/client/src/components/TestSuite/TestSuiteDetails/ResultIcon.tsx b/client/src/components/TestSuite/TestSuiteDetails/ResultIcon.tsx index 47360e507..e516c5fec 100644 --- a/client/src/components/TestSuite/TestSuiteDetails/ResultIcon.tsx +++ b/client/src/components/TestSuite/TestSuiteDetails/ResultIcon.tsx @@ -53,7 +53,7 @@ const ResultIcon: FC = ({ result, isRunning }) => { @@ -64,7 +64,7 @@ const ResultIcon: FC = ({ result, isRunning }) => { diff --git a/client/src/components/TestSuite/TestSuiteDetails/TestGroupCard.tsx b/client/src/components/TestSuite/TestSuiteDetails/TestGroupCard.tsx index c3445c787..1a2550df3 100644 --- a/client/src/components/TestSuite/TestSuiteDetails/TestGroupCard.tsx +++ b/client/src/components/TestSuite/TestSuiteDetails/TestGroupCard.tsx @@ -7,6 +7,7 @@ import InputOutputList from './TestListItem/InputOutputList'; import ResultIcon from './ResultIcon'; import TestRunButton from '~/components/TestSuite/TestRunButton/TestRunButton'; import { shouldShowDescription } from '~/components/TestSuite/TestSuiteUtilities'; +import remarkGfm from 'remark-gfm'; interface TestGroupCardProps { children: React.ReactNode; @@ -22,7 +23,9 @@ const TestGroupCard: FC = ({ children, runnable, runTests, v // render markdown once on mount - it's too slow with re-rendering const description = useMemo(() => { - return runnable.description ? {runnable.description} : undefined; + return runnable.description ? ( + {runnable.description} + ) : undefined; }, [runnable.description]); const runnableType = 'tests' in runnable ? RunnableType.TestGroup : RunnableType.TestSuite; diff --git a/client/src/components/TestSuite/TestSuiteDetails/TestListItem/TestListItem.tsx b/client/src/components/TestSuite/TestSuiteDetails/TestListItem/TestListItem.tsx index 9546e7d73..7fb97afb1 100644 --- a/client/src/components/TestSuite/TestSuiteDetails/TestListItem/TestListItem.tsx +++ b/client/src/components/TestSuite/TestSuiteDetails/TestListItem/TestListItem.tsx @@ -217,7 +217,11 @@ const TestListItem: FC = ({ TransitionProps={{ unmountOnExit: true }} onClick={handleAccordionClick} onKeyDown={(e) => { - if (e.key === 'Enter' || e.key === ' ') { + if (e.key === 'Enter') { + // Don't open/close accordion on enter + setTabIndex(findPopulatedTabIndex()); + } + if (e.key === ' ') { handleAccordionClick(); } }} diff --git a/client/src/components/TestSuite/TestSuiteDetails/TestListItem/__tests__/RequestList.test.tsx b/client/src/components/TestSuite/TestSuiteDetails/TestListItem/__tests__/RequestList.test.tsx index e13fe6182..d7ca0a789 100644 --- a/client/src/components/TestSuite/TestSuiteDetails/TestListItem/__tests__/RequestList.test.tsx +++ b/client/src/components/TestSuite/TestSuiteDetails/TestListItem/__tests__/RequestList.test.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import { act } from 'react-dom/test-utils'; import { vi } from 'vitest'; import userEvent from '@testing-library/user-event'; import { render, screen, waitFor } from '@testing-library/react'; @@ -12,15 +13,17 @@ import { } from '~/components/RequestDetailModal/__mocked_data__/mockData'; describe('The RequestsList component', () => { - test('it orders requests based on their index', () => { + test('it orders requests based on their index', async () => { const requests = [codeResponseWithHTML, mockedRequest]; - render( - - - {}} view="run" /> - - + await act(() => + render( + + + {}} view="run" /> + + + ) ); const renderedRequests = document.querySelectorAll('tbody > tr'); @@ -43,12 +46,14 @@ describe('The RequestsList component', () => { }, }); - render( - - - {}} view="run" /> - - + await act(() => + render( + + + {}} view="run" /> + + + ) ); const buttons = screen.getAllByRole('button'); @@ -64,15 +69,17 @@ describe('The RequestsList component', () => { vi.resetAllMocks(); }); - test('shows details when button is clicked', () => { + test('shows details when button is clicked', async () => { const requests = [codeResponseWithHTML, mockedRequest]; - render( - - - {}} view="run" /> - - + await act(() => + render( + + + {}} view="run" /> + + + ) ); const buttons = screen.getAllByRole('button'); diff --git a/client/src/components/TestSuite/__tests__/TestSession.test.tsx b/client/src/components/TestSuite/__tests__/TestSession.test.tsx index a6740bc61..a587465e3 100644 --- a/client/src/components/TestSuite/__tests__/TestSession.test.tsx +++ b/client/src/components/TestSuite/__tests__/TestSession.test.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { BrowserRouter } from 'react-router-dom'; +import { act } from 'react-dom/test-utils'; import { SnackbarProvider } from 'notistack'; import { render, screen } from '@testing-library/react'; import { vi } from 'vitest'; @@ -10,42 +11,46 @@ import TestSessionWrapper from '../TestSessionWrapper'; import { mockedTestSession, mockedResultsList } from '../__mocked_data__/mockData'; describe('The TestSession Component', () => { - it('renders TestSessionWrapper', () => { + it('renders TestSessionWrapper', async () => { const getCoreVersion = vi.spyOn(versionsApi, 'getCoreVersion'); getCoreVersion.mockResolvedValue('1.2.34'); - render( - - - - - - - + await act(() => + render( + + + + + + + + ) ); expect(getCoreVersion).toBeCalledTimes(1); }); - it('renders TestSession', () => { + it('renders TestSession', async () => { let drawerOpen = true; - render( - - - - {}} - drawerOpen={drawerOpen} - toggleDrawer={() => (drawerOpen = !drawerOpen)} - /> - - - + await act(() => + render( + + + + {}} + drawerOpen={drawerOpen} + toggleDrawer={() => (drawerOpen = !drawerOpen)} + /> + + + + ) ); const testSessionTitleComponentList = screen.getAllByTestId('navigable-group-item'); diff --git a/client/src/components/TestSuite/__tests__/TestSessionSmall.test.tsx b/client/src/components/TestSuite/__tests__/TestSessionSmall.test.tsx index 4dab73d42..441b3663c 100644 --- a/client/src/components/TestSuite/__tests__/TestSessionSmall.test.tsx +++ b/client/src/components/TestSuite/__tests__/TestSessionSmall.test.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { MemoryRouter } from 'react-router-dom'; +import { act } from 'react-dom/test-utils'; import { render, renderHook, screen } from '@testing-library/react'; import { SnackbarProvider } from 'notistack'; import ThemeProvider from 'components/ThemeProvider'; @@ -13,25 +14,27 @@ beforeEach(() => { result.current.windowIsSmall = true; }); -test('renders narrow screen TestSession', () => { +test('renders narrow screen TestSession', async () => { let drawerOpen = false; - render( - - - - {}} - drawerOpen={drawerOpen} - toggleDrawer={() => (drawerOpen = !drawerOpen)} - /> - - - + await act(() => + render( + + + + {}} + drawerOpen={drawerOpen} + toggleDrawer={() => (drawerOpen = !drawerOpen)} + /> + + + + ) ); const testSessionTitleComponentList = screen.getAllByTestId('navigable-group-item'); diff --git a/client/src/components/_common/ActionModal.tsx b/client/src/components/_common/ActionModal.tsx index 45533a81f..9eb82fac4 100644 --- a/client/src/components/_common/ActionModal.tsx +++ b/client/src/components/_common/ActionModal.tsx @@ -31,6 +31,7 @@ const ActionModal: FC = ({ modalVisible, message, cancelTestRu disableElevation onClick={cancelTestRun} data-testid="cancel-button" + sx={{ fontWeight: 'bold' }} > Cancel diff --git a/client/src/components/_common/CustomTab.tsx b/client/src/components/_common/CustomTab.tsx index 507bc4d6c..e59a070f9 100644 --- a/client/src/components/_common/CustomTab.tsx +++ b/client/src/components/_common/CustomTab.tsx @@ -10,7 +10,7 @@ interface CustomTabProps { const CustomTab = styled((props: CustomTabProps) => )( ({ theme }) => ({ pointerEvents: 'auto', - fontWeight: 'bolder', + fontWeight: 'bold', '&:hover, :focus-within': { color: theme.palette.common.grayDarkest, }, diff --git a/client/src/components/_common/SelectionPanel/RadioSelection.tsx b/client/src/components/_common/SelectionPanel/RadioSelection.tsx index 8b6bff6af..bf9f6673f 100644 --- a/client/src/components/_common/SelectionPanel/RadioSelection.tsx +++ b/client/src/components/_common/SelectionPanel/RadioSelection.tsx @@ -3,6 +3,7 @@ import { Box, FormControl, FormControlLabel, FormLabel, Radio, RadioGroup } from import HelpOutlineOutlinedIcon from '@mui/icons-material/HelpOutlineOutlined'; import { RadioOption, RadioOptionSelection } from '~/models/selectionModels'; import CustomTooltip from '~/components/_common/CustomTooltip'; +import useStyles from '~/components/_common/SelectionPanel/styles'; export interface RadioSelectionProps { options: RadioOption[]; @@ -13,6 +14,7 @@ const RadioSelection: FC = ({ options, setSelections: setParentSelections, }) => { + const { classes } = useStyles(); const initialSelectedRadioOptions: RadioOptionSelection[] = options.map((option) => ({ // just grab the first to start // perhaps choices should be persisted in the URL to make it easy to share specific options @@ -39,7 +41,12 @@ const RadioSelection: FC = ({ return ( {options.map((option, i) => ( - + {option.title || option.id} {option.description && ( diff --git a/client/src/components/_common/SelectionPanel/styles.tsx b/client/src/components/_common/SelectionPanel/styles.tsx index 2c2e69625..9b7d2b5bf 100644 --- a/client/src/components/_common/SelectionPanel/styles.tsx +++ b/client/src/components/_common/SelectionPanel/styles.tsx @@ -2,6 +2,11 @@ import { Theme } from '@mui/material/styles'; import { makeStyles } from 'tss-react/mui'; export default makeStyles()((theme: Theme) => ({ + label: { + '& label.Mui-focused': { + color: theme.palette.common.orangeDarkest, + }, + }, optionsList: { display: 'flex', flexDirection: 'column', diff --git a/client/src/index.css b/client/src/index.css index 046864a33..981b5373a 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -22,4 +22,16 @@ body { overscroll-behavior-y: none; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; +} + +/* Keyboard-only focus indicator */ +button:focus-visible { + border: 1px solid #707070; +} + +/* Darken button backgrounds on hover on text styled buttons */ +button:hover { + &.MuiButton-text { + background-color: #cbd5df60; + } } \ No newline at end of file diff --git a/codecov.yml b/codecov.yml index 67107dee2..78b517594 100644 --- a/codecov.yml +++ b/codecov.yml @@ -5,7 +5,7 @@ coverage: flag_management: default_rules: - carryforward: true; + carryforward: true individual_flags: - name: backend paths: diff --git a/dev_suites/dev_demo_ig_stu1/groups/demo_group.rb b/dev_suites/dev_demo_ig_stu1/groups/demo_group.rb index 3ea8819fd..330ebfd4b 100644 --- a/dev_suites/dev_demo_ig_stu1/groups/demo_group.rb +++ b/dev_suites/dev_demo_ig_stu1/groups/demo_group.rb @@ -9,6 +9,14 @@ class DemoGroup < Inferno::TestGroup # This is a markdown header **Inferno** [github](https://github.com/inferno-framework/inferno-core) + Below is a markdown table + | Column 1 | Column 2 | Column 3 | + | :--- | :---: | ---: | + | Entry 1 | Entry 2 | Entry 3| + | Entry 4 | Entry 5 | Entry 6 | + + This is a dummy canonical link http://hl7.org/fhir/ValueSet/my-valueset|0.8 that should not be + interpreted as a table ) # Inputs and outputs diff --git a/docs/Gemfile.lock b/docs/Gemfile.lock index e5cf68001..5295c70ee 100644 --- a/docs/Gemfile.lock +++ b/docs/Gemfile.lock @@ -14,7 +14,7 @@ GEM execjs coffee-script-source (1.11.1) colorator (1.1.0) - commonmarker (0.23.9) + commonmarker (0.23.10) concurrent-ruby (1.2.2) dnsruby (1.61.9) simpleidn (~> 0.1) diff --git a/inferno_core.gemspec b/inferno_core.gemspec index e3fb21184..b49d64cf8 100644 --- a/inferno_core.gemspec +++ b/inferno_core.gemspec @@ -11,7 +11,7 @@ Gem::Specification.new do |spec| spec.description = 'Inferno Core is an open source tool for testing data exchanges enabled by the FHIR standand' spec.homepage = 'https://github.com/inferno-framework/inferno-core' spec.license = 'Apache-2.0' - spec.add_runtime_dependency 'activesupport', '~> 6.1' + spec.add_runtime_dependency 'activesupport', '~> 6.1.7.5' spec.add_runtime_dependency 'base62-rb', '0.3.1' spec.add_runtime_dependency 'blueprinter', '0.25.2' spec.add_runtime_dependency 'dotenv', '~> 2.7' @@ -28,7 +28,7 @@ Gem::Specification.new do |spec| spec.add_runtime_dependency 'oj', '3.11.0' spec.add_runtime_dependency 'pry' spec.add_runtime_dependency 'pry-byebug' - spec.add_runtime_dependency 'puma', '~> 5.3' + spec.add_runtime_dependency 'puma', '~> 5.6.7' spec.add_runtime_dependency 'rake', '~> 13.0' spec.add_runtime_dependency 'sequel', '~> 5.42.0' spec.add_runtime_dependency 'sidekiq', '~> 6.5.6' diff --git a/lib/inferno/apps/cli/main.rb b/lib/inferno/apps/cli/main.rb index 07e5368e5..ad7f1c353 100644 --- a/lib/inferno/apps/cli/main.rb +++ b/lib/inferno/apps/cli/main.rb @@ -36,7 +36,7 @@ def start command = "rerun \"#{command}\" --background" end - system command + exec command end desc 'suites', 'List available test suites' diff --git a/lib/inferno/config/boot/suites.rb b/lib/inferno/config/boot/suites.rb index b698b3e55..0e621b774 100644 --- a/lib/inferno/config/boot/suites.rb +++ b/lib/inferno/config/boot/suites.rb @@ -25,5 +25,13 @@ end ObjectSpace.each_object(TracePoint, &:disable) + + Inferno::Entities::TestSuite.descendants.each do |descendant| + # When ID not assigned in custom test suites, Runnable.id will return default ID + # equal to the custom test suite's parent class name + if descendant.id.blank? || descendant.id == 'Inferno::Entities::TestSuite' + raise StandardError, "Error initializing test suite #{descendant.name}: test suite ID is not set" + end + end end end diff --git a/lib/inferno/dsl/fhir_validation.rb b/lib/inferno/dsl/fhir_validation.rb index 7f1ae81c2..5a8436980 100644 --- a/lib/inferno/dsl/fhir_validation.rb +++ b/lib/inferno/dsl/fhir_validation.rb @@ -116,14 +116,22 @@ def exclude_message(&block) def resource_is_valid?(resource, profile_url, runnable) profile_url ||= FHIR::Definitions.resource_definition(resource.resourceType).url - outcome, http_status = validate(resource, profile_url, runnable) + begin + response = call_validator(resource, profile_url) + rescue StandardError => e + # This could be a complete failure to connect (validator isn't running) + # or a timeout (validator took too long to respond). + runnable.add_message('error', e.message) + raise Inferno::Exceptions::ErrorInValidatorException, "Unable to connect to validator at #{url}." + end + outcome = operation_outcome_from_validator_response(response.body, runnable) message_hashes = message_hashes_from_outcome(outcome, resource, profile_url) message_hashes .each { |message_hash| runnable.add_message(message_hash[:type], message_hash[:message]) } - unless http_status == 200 + unless response.status == 200 raise Inferno::Exceptions::ErrorInValidatorException, 'Error occurred in the validator. Review Messages tab or validator service logs for more information.' end @@ -191,21 +199,17 @@ def issue_message(issue, resource) # # @param resource [FHIR::Model] # @param profile_url [String] - # @param runnable [Inferno::Entities::Test] - # @return [[Array(FHIR::OperationOutcome, Number)] the validation response and HTTP status code - def validate(resource, profile_url, runnable) - begin - response = Faraday.new( - url, - params: { profile: profile_url } - ).post('validate', resource.source_contents) - rescue StandardError => e - runnable.add_message('error', e.message) - raise Inferno::Exceptions::ErrorInValidatorException, "Unable to connect to validator at #{url}." - end - outcome = operation_outcome_from_validator_response(response.body, runnable) + # @return [String] the body of the validation response + def validate(resource, profile_url) + call_validator(resource, profile_url).body + end - [outcome, response.status] + # @private + def call_validator(resource, profile_url) + Faraday.new( + url, + params: { profile: profile_url } + ).post('validate', resource.source_contents) end # @private @@ -213,7 +217,7 @@ def operation_outcome_from_validator_response(response, runnable) if response.start_with? '{' FHIR::OperationOutcome.new(JSON.parse(response)) else - runnable.add_message('error', "Validator Response: #{response}") + runnable.add_message('error', "Validator Response:\n#{response}") raise Inferno::Exceptions::ErrorInValidatorException, 'Validator response was an unexpected format. '\ 'Review Messages tab or validator service logs for more information.' diff --git a/lib/inferno/exceptions.rb b/lib/inferno/exceptions.rb index bcd059594..47e90dc41 100644 --- a/lib/inferno/exceptions.rb +++ b/lib/inferno/exceptions.rb @@ -39,11 +39,14 @@ def result end end + # ErrorInValidatorException is used when an exception occurred in + # calling the validator service, for example a connection timeout + # or an unexpected response format. + # Note: This class extends TestResultException instead of RuntimeError + # to bypass printing the stack trace in the UI, since + # the stack trace of this exception is not likely be useful. + # Instead the message should point to where in the validator an error occurred. class ErrorInValidatorException < TestResultException - # This extends TestResultException instead of RuntimeError - # to bypass printing the stack trace in the UI. - # (The stack trace of this exception may not be useful, - # instead the message should point to where in the validator an error occurred) def result 'error' end diff --git a/lib/inferno/version.rb b/lib/inferno/version.rb index 0f6f1b3fd..5eaa87402 100644 --- a/lib/inferno/version.rb +++ b/lib/inferno/version.rb @@ -1,4 +1,4 @@ module Inferno # Standard patterns for gem versions: https://guides.rubygems.org/patterns/ - VERSION = '0.4.18'.freeze + VERSION = '0.4.20'.freeze end