Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
a1b40a2
Enhance input visibility logic and add new helper functions
projkov Jan 31, 2026
c3bb5f9
Refactor input handling and enhance test coverage
projkov Jan 31, 2026
0b17e40
Enhance input serializer and entity to support conditional display
projkov Jan 31, 2026
8d004a6
Add conditional input group to demo suite
projkov Jan 31, 2026
098bda9
Initialize Bundler in inferno script
projkov Jan 31, 2026
87e3a29
Remove yarn.lock file from the project
projkov Jan 31, 2026
983a165
Fix lint errors in the client
projkov Feb 1, 2026
01b1a04
Refactor input visibility logic to use `enable_when` instead of `show…
projkov Feb 2, 2026
bbf9490
Change conditional input group in demo suite
projkov Feb 2, 2026
e1913e4
Refactor input handling and enhance conditional input tests
projkov Feb 6, 2026
1de06b6
Remove Bundler setup requirement from the inferno script
projkov Feb 6, 2026
77fff3b
Update normalizeValue function and add comprehensive tests
projkov Feb 6, 2026
ebf97db
Merge branch 'main' into conditional-rendering-of-modal-inputs
projkov May 13, 2026
4cf6082
Remove unused sortAndNormalizeArray function from InputHelpers.ts
projkov May 13, 2026
f81cbaf
Add test to cover dependent visibility for checkbox
projkov May 13, 2026
a3cd415
Update conditionalShowInput documentation for clarity and accuracy
projkov May 13, 2026
7810955
Remove code duplication in Inputs.test.tsx
projkov May 13, 2026
33b30e3
Enhance InputHelpers with array sorting in normalization and add cons…
projkov May 13, 2026
a27cff2
Lint auto fixes
projkov May 20, 2026
aaaf1e2
Lint fixes
projkov May 20, 2026
323298d
Remove console warnings for missing inputs in conditionalShowInput an…
projkov May 20, 2026
73af7d3
Update conditionalShowInput to account for checkbox type and remove u…
projkov May 20, 2026
e7fba3c
Remove console warning check for missing inputs in conditionalShowInp…
projkov May 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,7 @@ PLATFORMS
arm64-darwin-22
arm64-darwin-23
arm64-darwin-24
arm64-darwin-25
x86_64-darwin-20
x86_64-darwin-22
x86_64-darwin-23
Expand Down
3 changes: 1 addition & 2 deletions client/src/components/Header/HelpModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import {
Typography,
} from '@mui/material';
import ResultIcon from '~/components/TestSuite/TestSuiteDetails/ResultIcon';
import { Result } from '~/models/testSuiteModels';
import lightTheme from '~/styles/theme';

export interface HelpModalProps {
Expand Down Expand Up @@ -138,7 +137,7 @@ const HelpModal: FC<HelpModalProps> = ({ hideModal, modalVisible }) => {
updated_at: '',
outputs: [],
optional: row.optional,
} as Result
}
}
isRunning={row.pending}
/>
Expand Down
2 changes: 1 addition & 1 deletion client/src/components/InputsModal/Auth/InputAuth.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ const InputAuth: FC<InputAuthProps> = ({ mode, input, index, inputsMap, setInput
...inputDefaultValues,
...inputStartingValues,
...inputsMapValues,
} as Auth;
};
};

const updateAuthType = (map: Map<string, unknown>) => {
Expand Down
2 changes: 1 addition & 1 deletion client/src/components/InputsModal/InputCheckboxGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ const InputCheckboxGroup: FC<InputCheckboxGroupProps> = ({
{} as CheckboxValues,
);
}
return startingValues as CheckboxValues;
return startingValues;
});

const isMissingInput = hasBeenModified && !input.optional && inputsMap.get(input.name) === '[]';
Expand Down
3 changes: 2 additions & 1 deletion client/src/components/InputsModal/InputFields.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import InputOAuthCredentials from '~/components/InputsModal/InputOAuthCredential
import InputRadioGroup from '~/components/InputsModal/InputRadioGroup';
import InputSingleCheckbox from '~/components/InputsModal/InputSingleCheckbox';
import InputTextField from '~/components/InputsModal/InputTextField';
import { showInput } from './InputHelpers';

export interface InputFieldsProps {
inputs: TestInput[];
Expand All @@ -19,7 +20,7 @@ const InputFields: FC<InputFieldsProps> = ({ inputs, inputsMap, setInputsMap })
return (
<List>
{inputs.map((input: TestInput, index: number) => {
if (!input.hidden) {
if (showInput(input, inputsMap)) {
switch (input.type) {
case 'auth_info':
if (input.options?.mode === 'auth') {
Expand Down
61 changes: 61 additions & 0 deletions client/src/components/InputsModal/InputHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,3 +171,64 @@ export const isJsonString = (str: unknown) => {
}
return !!value && typeof value === 'object';
};

/**
* Converts a value to its string representation.
* - If value is null or undefined, returns an empty string.
* - Arrays are sorted before JSON-stringifying so that selection order does not
* affect equality (e.g. checkbox values `['b','a']` and `['a','b']` are equal).
* - Other objects are JSON-stringified as-is.
*
* @param value - The value to normalize.
* @returns The normalized string value.
*/
export const normalizeValue = (value: unknown): string => {
if (value === null) {
return '';
}
switch (typeof value) {
case 'string':
return value;
case 'number':
case 'boolean':
case 'bigint':
case 'symbol':
return String(value);
case 'object':
if (Array.isArray(value)) {
return JSON.stringify([...(value as unknown[])].sort());
}
return JSON.stringify(value);
default:
return '';
}
};

/**
* Returns whether the input should be shown from its `enable_when` rule and `inputsMap`.
*
* - No `enable_when`, or no `input_name` on it → always show (rule ignored).
* - With `input_name`: hide when the referenced key is absent from `inputsMap` (`undefined`).
* A console warning is emitted in this case to help catch authoring mistakes (e.g. typos).
* - Otherwise show when {@link normalizeValue} of the referenced value equals
* {@link normalizeValue} of `enable_when.value` (arrays are sorted before comparison, so
* checkbox selection order does not matter; other objects are compared via JSON.stringify).
*/
export const conditionalShowInput = (
input: TestInput,
inputsMap: Map<string, unknown>,
): boolean => {
const enableWhen = input.enable_when;
if (!enableWhen?.input_name || input.type === 'checkbox') {
return true;
}
const inputValue = inputsMap.get(enableWhen.input_name);
if (inputValue === undefined) {
return false;
}
return normalizeValue(inputValue) === normalizeValue(enableWhen.value);
};

export const showInput = (input: TestInput, inputsMap: Map<string, unknown>): boolean => {
return !input.hidden && conditionalShowInput(input, inputsMap);
};
2 changes: 1 addition & 1 deletion client/src/components/InputsModal/InputRadioGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ const InputRadioGroup: FC<InputRadioGroupProps> = ({ input, index, inputsMap, se
row
aria-label={`${input.name}-radio-buttons-group`}
name={`${input.name}-radio-buttons-group`}
value={inputsMap.get(input.name) || (input.default as string) || firstOptionValue}
value={inputsMap.get(input.name) || input.default || firstOptionValue}
onChange={(event) => {
inputsMap.set(input.name, event.target.value);
setInputsMap(new Map(inputsMap));
Expand Down
123 changes: 123 additions & 0 deletions client/src/components/InputsModal/__tests__/InputHelpers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { describe, expect, it } from 'vitest';
import {
normalizeValue,
conditionalShowInput,
showInput,
} from '~/components/InputsModal/InputHelpers';
import { TestInput } from '~/models/testSuiteModels';

describe('normalizeValue', () => {
it('returns empty string for null', () => {
expect(normalizeValue(null)).toBe('');
});

it('returns empty string for undefined', () => {
expect(normalizeValue(undefined)).toBe('');
});

it('returns string unchanged', () => {
expect(normalizeValue('')).toBe('');
expect(normalizeValue('hello')).toBe('hello');
expect(normalizeValue('4.0')).toBe('4.0');
});

it('converts number to string', () => {
expect(normalizeValue(0)).toBe('0');
expect(normalizeValue(42)).toBe('42');
expect(normalizeValue(-1)).toBe('-1');
expect(normalizeValue(3.14)).toBe('3.14');
});

it('converts boolean to string', () => {
expect(normalizeValue(true)).toBe('true');
expect(normalizeValue(false)).toBe('false');
});

it('converts bigint to string', () => {
expect(normalizeValue(BigInt(0))).toBe('0');
expect(normalizeValue(BigInt(9007199254740991))).toBe('9007199254740991');
});

it('converts symbol to string', () => {
const sym = Symbol('test');
expect(normalizeValue(sym)).toBe(sym.toString());
});

it('JSON-stringifies plain objects', () => {
expect(normalizeValue({})).toBe('{}');
expect(normalizeValue({ a: 1, b: 'x' })).toBe('{"a":1,"b":"x"}');
});

it('JSON-stringifies arrays in sorted order', () => {
expect(normalizeValue([])).toBe('[]');
expect(normalizeValue(['a', 'b', 'c'])).toBe('["a","b","c"]');
expect(normalizeValue(['c', 'b', 'a'])).toBe('["a","b","c"]');
});

it('returns empty string for function (default case)', () => {
expect(normalizeValue(() => {})).toBe('');
});
});

const makeInput = (overrides: Partial<TestInput> = {}): TestInput => ({
name: 'dep',
type: 'text',
...overrides,
});

describe('conditionalShowInput', () => {
it('returns true when enable_when is absent', () => {
expect(conditionalShowInput(makeInput(), new Map())).toBe(true);
});

it('returns true when enable_when has no input_name', () => {
const input = makeInput({ enable_when: { input_name: '', value: 'x' } });
expect(conditionalShowInput(input, new Map())).toBe(true);
});

it('returns false and warns when input_name is not in the map', () => {
const input = makeInput({ enable_when: { input_name: 'missing', value: 'x' } });
const warned: string[] = [];
const originalWarn = console.warn;
console.warn = (...args: unknown[]) => warned.push(String(args[0]));
const result = conditionalShowInput(input, new Map());
console.warn = originalWarn;
expect(result).toBe(false);
});

it('returns false when value does not match', () => {
const input = makeInput({ enable_when: { input_name: 'ctrl', value: 'yes' } });
expect(conditionalShowInput(input, new Map([['ctrl', 'no']]))).toBe(false);
});

it('returns true when value matches', () => {
const input = makeInput({ enable_when: { input_name: 'ctrl', value: 'yes' } });
expect(conditionalShowInput(input, new Map([['ctrl', 'yes']]))).toBe(true);
});

it('returns true for checkbox array regardless of selection order', () => {
const input = makeInput({ enable_when: { input_name: 'ctrl', value: '["a","b"]' } });
expect(conditionalShowInput(input, new Map([['ctrl', ['b', 'a']]]))).toBe(true);
});
});

describe('showInput', () => {
it('returns false when hidden is true, even if enable_when matches', () => {
const input = makeInput({ hidden: true, enable_when: { input_name: 'ctrl', value: 'yes' } });
expect(showInput(input, new Map([['ctrl', 'yes']]))).toBe(false);
});

it('returns true when not hidden and no enable_when', () => {
expect(showInput(makeInput(), new Map())).toBe(true);
});

it('returns true when not hidden and enable_when matches', () => {
const input = makeInput({ enable_when: { input_name: 'ctrl', value: 'yes' } });
expect(showInput(input, new Map([['ctrl', 'yes']]))).toBe(true);
});

it('returns false when not hidden but enable_when does not match', () => {
const input = makeInput({ enable_when: { input_name: 'ctrl', value: 'yes' } });
expect(showInput(input, new Map([['ctrl', 'no']]))).toBe(false);
});
});
Loading
Loading