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