remove UI spec and refactor ui_spec.order to input_field_order property

This commit is contained in:
Emily Soth 2025-05-08 11:50:38 -07:00
parent deb37d9c5c
commit 6639e0ff73
21 changed files with 96 additions and 142 deletions

View File

@ -1141,16 +1141,12 @@ class OptionStringOutput(Output):
"""
options: typing.Union[list, None] = None
@dataclasses.dataclass
class UISpec:
order: typing.Union[list, None] = None
@dataclasses.dataclass
class ModelSpec:
model_id: str
model_title: str
userguide: str
ui_spec: UISpec
input_field_order: list[str]
inputs: typing.Iterable[Input]
outputs: typing.Iterable[Output]
args_with_spatial_overlap: dict
@ -1213,16 +1209,14 @@ def build_model_spec(model_spec):
for argkey, argspec in model_spec['args'].items()]
outputs = [
build_output_spec(argkey, argspec) for argkey, argspec in model_spec['outputs'].items()]
ui_spec = UISpec(
order=model_spec['ui_spec']['order'])
return ModelSpec(
model_id=model_spec['model_id'],
model_title=model_spec['model_title'],
userguide=model_spec['userguide'],
aliases=model_spec['aliases'],
ui_spec=ui_spec,
inputs=inputs,
outputs=outputs,
input_field_order=model_spec['ui_spec']['order'],
args_with_spatial_overlap=model_spec.get('args_with_spatial_overlap', None))

View File

@ -27,6 +27,6 @@ MODEL_SPEC = spec.ModelSpec(inputs=[
model_id='',
model_title='',
userguide='',
ui_spec=spec.UISpec(),
input_field_order=[],
args_with_spatial_overlap={}
)

View File

@ -9,6 +9,6 @@ MODEL_SPEC = spec.ModelSpec(
model_id='',
model_title='',
userguide='',
ui_spec=spec.UISpec(),
input_field_order=[],
args_with_spatial_overlap={}
)

View File

@ -9,6 +9,6 @@ MODEL_SPEC = spec.ModelSpec(inputs=[
model_id='',
model_title='',
userguide='',
ui_spec=spec.UISpec(),
input_field_order=[],
args_with_spatial_overlap={}
)

View File

@ -6,6 +6,6 @@ MODEL_SPEC = spec.ModelSpec(inputs=[
model_id='',
model_title='',
userguide='',
ui_spec=spec.UISpec(),
input_field_order=[],
args_with_spatial_overlap={}
)

View File

@ -13,6 +13,6 @@ MODEL_SPEC = spec.ModelSpec(inputs=[
model_id='',
model_title='',
userguide='',
ui_spec=spec.UISpec(),
input_field_order=[],
args_with_spatial_overlap={}
)

View File

@ -7,6 +7,6 @@ MODEL_SPEC = SimpleNamespace(inputs=[
model_id='',
model_title='',
userguide='',
ui_spec=spec.UISpec(),
input_field_order=[],
args_with_spatial_overlap={}
)

View File

@ -7,6 +7,6 @@ MODEL_SPEC = spec.ModelSpec(inputs=[
model_id='',
model_title='',
userguide='',
ui_spec=spec.UISpec(),
input_field_order=[],
args_with_spatial_overlap={}
)

View File

@ -121,7 +121,7 @@ class ValidateModelSpecs(unittest.TestCase):
"""MODEL_SPEC: test each spec meets the expected pattern."""
required_keys = {'model_id', 'model_title', 'userguide',
'aliases', 'inputs', 'ui_spec', 'outputs'}
'aliases', 'inputs', 'input_field_order', 'outputs'}
for model_id, pyname in model_id_to_pyname.items():
model = importlib.import_module(pyname)
@ -137,10 +137,9 @@ class ValidateModelSpecs(unittest.TestCase):
set(model.MODEL_SPEC.args_with_spatial_overlap).issubset(
{'spatial_keys', 'different_projections_ok'}))
self.assertIsInstance(model.MODEL_SPEC.ui_spec, spec.UISpec)
self.assertIsInstance(model.MODEL_SPEC.ui_spec.order, list)
self.assertIsInstance(model.MODEL_SPEC.input_field_order, list)
found_keys = set()
for group in model.MODEL_SPEC.ui_spec.order:
for group in model.MODEL_SPEC.input_field_order:
self.assertIsInstance(group, list)
for key in group:
self.assertIsInstance(key, str)

View File

@ -361,7 +361,7 @@ class TestMetadataFromSpec(unittest.TestCase):
model_title='Urban Nature Access',
userguide='',
aliases=[],
ui_spec={},
input_field_order=[],
inputs={},
args_with_spatial_overlap={},
outputs=output_spec

View File

@ -45,7 +45,7 @@ class EndpointFunctionTests(unittest.TestCase):
self.assertEqual(
set(spec),
{'model_id', 'model_title', 'userguide', 'aliases',
'ui_spec', 'args_with_spatial_overlap', 'args', 'outputs'})
'input_field_order', 'args_with_spatial_overlap', 'args', 'outputs'})
def test_get_invest_validate(self):
"""UI server: get_invest_validate endpoint."""

View File

@ -66,8 +66,7 @@ class UsageLoggingTests(unittest.TestCase):
}
model_spec = spec.ModelSpec(
model_id='', model_title='', userguide=None,
aliases=None, ui_spec=spec.UISpec(order=[]),
model_id='', model_title='', userguide=None, aliases=None,
inputs=[
spec.SingleBandRasterInput(id='raster', band=spec.Input()),
spec.VectorInput(id='vector', geometries={}, fields={}),
@ -76,6 +75,7 @@ class UsageLoggingTests(unittest.TestCase):
spec.VectorInput(id='blank_vector_path', geometries={}, fields={})
],
outputs={},
input_field_order=[],
args_with_spatial_overlap=None)
output_logfile = os.path.join(self.workspace_dir, 'logfile.txt')

View File

@ -22,7 +22,6 @@ from natcap.invest import spec
from natcap.invest.spec import (
u,
ModelSpec,
UISpec,
Input,
FileInput,
CSVInput,
@ -40,14 +39,12 @@ from natcap.invest.spec import (
gdal.UseExceptions()
def ui_spec_with_defaults(order=[]):
return UISpec(order=order)
def model_spec_with_defaults(model_id='', model_title='', userguide='', aliases=None,
ui_spec=ui_spec_with_defaults(), inputs={}, outputs={},
inputs={}, outputs={}, input_field_order=[],
args_with_spatial_overlap=[]):
return ModelSpec(model_id=model_id, model_title=model_title, userguide=userguide,
aliases=aliases, ui_spec=ui_spec, inputs=inputs, outputs=outputs,
aliases=aliases, inputs=inputs, outputs=outputs,
input_field_order=input_field_order,
args_with_spatial_overlap=args_with_spatial_overlap)
def number_input_spec_with_defaults(id='', units=u.none, expression='', **kwargs):

View File

@ -37,7 +37,6 @@ class InvestTab extends React.Component {
activeTab: 'setup',
modelSpec: null, // MODEL_SPEC dict with all keys except MODEL_SPEC.args
argsSpec: null, // MODEL_SPEC.args, the immutable args stuff
uiSpec: null,
userTerminated: false,
executeClicked: false,
tabStatus: '',
@ -74,13 +73,10 @@ class InvestTab extends React.Component {
}
}
try {
const {
args, ui_spec, ...model_spec
} = await getSpec(job.modelID);
const { args, ...model_spec } = await getSpec(job.modelID);
this.setState({
modelSpec: model_spec,
argsSpec: args,
uiSpec: ui_spec,
}, () => { this.switchTabs('setup'); });
} catch (error) {
console.log(error);
@ -211,7 +207,6 @@ class InvestTab extends React.Component {
activeTab,
modelSpec,
argsSpec,
uiSpec,
executeClicked,
tabStatus,
showErrorModal,
@ -310,7 +305,7 @@ class InvestTab extends React.Component {
userguide={modelSpec.userguide}
modelID={modelID}
argsSpec={argsSpec}
uiSpec={uiSpec}
inputFieldOrder={modelSpec.input_field_order}
argsInitValues={argsValues}
investExecute={this.investExecute}
sidebarSetupElementId={sidebarSetupElementId}

View File

@ -35,7 +35,7 @@ const { logger } = window.Workbench;
* Values initialize with either a complete args dict, or with empty/default values.
*
* @param {object} argsSpec - an InVEST model's MODEL_SPEC.args
* @param {object} uiSpec - the model's UI Spec.
* @param {object} inputFieldOrder - the order in which to display the input fields.
* @param {object} argsDict - key: value pairs of InVEST model arguments, or {}.
*
* @returns {object} to destructure into two args,
@ -45,11 +45,11 @@ const { logger } = window.Workbench;
* {object} argsDropdownOptions - stores lists of dropdown options for
* args of type 'option_string'.
*/
function initializeArgValues(argsSpec, uiSpec, argsDict) {
function initializeArgValues(argsSpec, inputFieldOrder, argsDict) {
const initIsEmpty = Object.keys(argsDict).length === 0;
const argsValues = {};
const argsDropdownOptions = {};
uiSpec.order.flat().forEach((argkey) => {
inputFieldOrder.flat().forEach((argkey) => {
// When initializing with undefined values, assign defaults so that,
// a) values are handled well by the html inputs and
// b) the object exported to JSON on "Save" or "Execute" includes defaults.
@ -129,12 +129,12 @@ class SetupTab extends React.Component {
* not on every re-render.
*/
this._isMounted = true;
const { argsInitValues, argsSpec, uiSpec } = this.props;
const { argsInitValues, argsSpec, inputFieldOrder } = this.props;
const {
argsValues,
argsDropdownOptions,
} = initializeArgValues(argsSpec, uiSpec, argsInitValues || {});
} = initializeArgValues(argsSpec, inputFieldOrder, argsInitValues || {});
// map each arg to an empty object, to fill in later
// here we use the argsSpec because it includes all args, even ones like
@ -143,10 +143,10 @@ class SetupTab extends React.Component {
acc[argkey] = {};
return acc;
}, {});
// here we only use the keys in uiSpec.order because args that
// here we only use the keys in inputFieldOrder because args that
// aren't displayed in the form don't need an enabled/disabled state.
// all args default to being enabled
const argsEnabled = uiSpec.order.flat().reduce((acc, argkey) => {
const argsEnabled = inputFieldOrder.flat().reduce((acc, argkey) => {
acc[argkey] = true;
return acc;
}, {});
@ -355,7 +355,6 @@ class SetupTab extends React.Component {
* @returns {undefined}
*/
updateArgValues(key, value) {
const { uiSpec } = this.props;
const { argsValues } = this.state;
argsValues[key].value = value;
this.setState({
@ -372,11 +371,11 @@ class SetupTab extends React.Component {
* @param {object} argsDict - key: value pairs of InVEST arguments.
*/
batchUpdateArgs(argsDict) {
const { argsSpec, uiSpec } = this.props;
const { argsSpec, inputFieldOrder } = this.props;
const {
argsValues,
argsDropdownOptions,
} = initializeArgValues(argsSpec, uiSpec, argsDict);
} = initializeArgValues(argsSpec, inputFieldOrder, argsDict);
this.setState({
argsValues: argsValues,
@ -539,10 +538,10 @@ class SetupTab extends React.Component {
const {
argsSpec,
userguide,
inputFieldOrder,
sidebarSetupElementId,
sidebarFooterElementId,
executeClicked,
uiSpec,
modelID,
} = this.props;
@ -594,7 +593,7 @@ class SetupTab extends React.Component {
argsValidation={argsValidation}
argsEnabled={argsEnabled}
argsDropdownOptions={argsDropdownOptions}
argsOrder={uiSpec.order}
argsOrder={inputFieldOrder}
userguide={userguide}
updateArgValues={this.updateArgValues}
updateArgTouched={this.updateArgTouched}
@ -660,9 +659,7 @@ SetupTab.propTypes = {
type: PropTypes.string,
})
).isRequired,
uiSpec: PropTypes.shape({
order: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.string)).isRequired,
}).isRequired,
inputFieldOrder: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.string)).isRequired,
argsInitValues: PropTypes.objectOf(PropTypes.oneOfType(
[PropTypes.string, PropTypes.bool, PropTypes.number])),
investExecute: PropTypes.func.isRequired,

View File

@ -214,7 +214,7 @@ describe('Test building model UIs and forum links', () => {
modelID={modelID}
argsSpec={argsSpec.args}
userguide={argsSpec.userguide}
uiSpec={argsSpec.ui_spec}
inputFieldOrder={argsSpec.input_field_order}
argsInitValues={undefined}
investExecute={() => {}}
nWorkers="-1"

View File

@ -51,9 +51,7 @@ const SAMPLE_SPEC = {
type: 'csv',
},
},
ui_spec: {
order: [['workspace_dir', 'carbon_pools_path']],
},
input_field_order: [['workspace_dir', 'carbon_pools_path']],
};
describe('Various ways to open and close InVEST models', () => {

View File

@ -24,7 +24,8 @@ import {
getInvestModelIDs,
getSpec,
fetchValidation,
fetchArgsEnabled
fetchArgsEnabled,
getDynamicDropdowns
} from '../../src/renderer/server_requests';
import InvestJob from '../../src/renderer/InvestJob';
@ -47,9 +48,7 @@ describe('InVEST subprocess testing', () => {
type: 'freestyle_string',
},
},
ui_spec: {
order: [['workspace_dir', 'results_suffix']],
},
input_field_order: [['workspace_dir', 'results_suffix']],
model_name: 'EcoModel',
pyname: 'natcap.invest.dot',
userguide: 'foo.html',
@ -111,6 +110,7 @@ describe('InVEST subprocess testing', () => {
fetchArgsEnabled.mockResolvedValue({
workspace_dir: true, results_suffix: true,
});
getDynamicDropdowns.mockResolvedValue({});
getInvestModelIDs.mockResolvedValue(
{ carbon: { model_title: modelTitle } }
);

View File

@ -15,7 +15,8 @@ import {
writeParametersToFile,
fetchValidation,
fetchDatastackFromFile,
fetchArgsEnabled
fetchArgsEnabled,
getDynamicDropdowns
} from '../../src/renderer/server_requests';
import InvestJob from '../../src/renderer/InvestJob';
import setupDialogs from '../../src/main/setupDialogs';
@ -51,9 +52,7 @@ describe('Run status Alert renders with status from a recent run', () => {
pyname: 'natcap.invest.foo',
model_title: 'Foo Model',
userguide: 'foo.html',
ui_spec: {
order: [['workspace']],
},
input_field_order: [['workspace']],
args: {
workspace: {
name: 'Workspace',
@ -67,6 +66,7 @@ describe('Run status Alert renders with status from a recent run', () => {
getSpec.mockResolvedValue(spec);
fetchValidation.mockResolvedValue([]);
fetchArgsEnabled.mockResolvedValue({ workspace: true });
getDynamicDropdowns.mockResolvedValue({});
setupDialogs();
});
@ -118,7 +118,7 @@ describe('Run status Alert renders with status from a recent run', () => {
describe('Open Workspace button', () => {
const spec = {
args: {},
ui_spec: { order: [] },
input_field_order: [],
};
const baseJob = {
@ -129,6 +129,7 @@ describe('Open Workspace button', () => {
beforeEach(() => {
getSpec.mockResolvedValue(spec);
fetchValidation.mockResolvedValue([]);
getDynamicDropdowns.mockResolvedValue({});
setupDialogs();
});
@ -184,9 +185,7 @@ describe('Sidebar Buttons', () => {
pyname: 'natcap.invest.foo',
model_title: 'Foo Model',
userguide: 'foo.html',
ui_spec: {
order: [['workspace', 'port']],
},
input_field_order: [['workspace', 'port']],
args: {
workspace: {
name: 'Workspace',
@ -204,6 +203,7 @@ describe('Sidebar Buttons', () => {
getSpec.mockResolvedValue(spec);
fetchValidation.mockResolvedValue([]);
fetchArgsEnabled.mockResolvedValue({ workspace: true, port: true });
getDynamicDropdowns.mockResolvedValue({});
setupOpenExternalUrl();
setupOpenLocalHtml();
ipcRenderer.invoke.mockImplementation((channel) => {
@ -547,9 +547,7 @@ describe('InVEST Run Button', () => {
pyname: 'natcap.invest.bar',
model_title: 'Bar Model',
userguide: 'bar.html',
ui_spec: {
order: [['a', 'b', 'c']],
},
input_field_order: [['a', 'b', 'c']],
args: {
a: {
name: 'abar',

View File

@ -34,9 +34,7 @@ describe('Add plugin modal', () => {
type: 'raster',
},
},
ui_spec: {
order: [['workspace_dir', 'input_path']],
},
input_field_order: [['workspace_dir', 'input_path']],
});
fetchArgsEnabled.mockResolvedValue({

View File

@ -31,9 +31,7 @@ const BASE_MODEL_SPEC = {
about: 'this is about foo',
},
},
ui_spec: {
order: [['arg']]
},
input_field_order: [['arg']],
};
/**
@ -55,7 +53,7 @@ const BASE_ARGS_ENABLED = {}
Object.keys(BASE_MODEL_SPEC.args).forEach((arg) => {
BASE_ARGS_ENABLED[arg] = true;
});
const UI_SPEC = { order: [Object.keys(BASE_MODEL_SPEC.args)] };
const INPUT_FIELD_ORDER = [Object.keys(BASE_MODEL_SPEC.args)];
/**
* Render a SetupTab component given the necessary specs.
@ -63,7 +61,7 @@ const UI_SPEC = { order: [Object.keys(BASE_MODEL_SPEC.args)] };
* @param {object} baseSpec - an invest model spec
* @returns {object} - containing the test utility functions returned by render
*/
function renderSetupFromSpec(baseSpec, uiSpec, initValues = undefined) {
function renderSetupFromSpec(baseSpec, inputFieldOrder, initValues = undefined) {
// some MODEL_SPEC boilerplate that is not under test,
// but is required by PropType-checking
const spec = { ...baseSpec };
@ -75,7 +73,7 @@ function renderSetupFromSpec(baseSpec, uiSpec, initValues = undefined) {
userguide={spec.userguide}
modelID={spec.model_id}
argsSpec={spec.args}
uiSpec={uiSpec}
inputFieldOrder={inputFieldOrder}
argsInitValues={initValues}
investExecute={() => {}}
nWorkers="-1"
@ -107,7 +105,7 @@ describe('Arguments form input types', () => {
const {
findByLabelText, findByRole,
} = renderSetupFromSpec(spec, UI_SPEC);
} = renderSetupFromSpec(spec, INPUT_FIELD_ORDER);
const input = await findByLabelText(RegExp(`^${spec.args.arg.name}`));
expect(input).toHaveAttribute('type', 'text');
@ -122,21 +120,21 @@ describe('Arguments form input types', () => {
['integer'],
])('render a text input for a %s', async (type) => {
const spec = baseArgsSpec(type);
const { findByLabelText } = renderSetupFromSpec(spec, UI_SPEC);
const { findByLabelText } = renderSetupFromSpec(spec, INPUT_FIELD_ORDER);
const input = await findByLabelText((content) => content.startsWith(spec.args.arg.name));
expect(input).toHaveAttribute('type', 'text');
});
test('render a text input with unit label for a number', async () => {
const spec = baseArgsSpec('number');
const { findByLabelText } = renderSetupFromSpec(spec, UI_SPEC);
const { findByLabelText } = renderSetupFromSpec(spec, INPUT_FIELD_ORDER);
const input = await findByLabelText(`${spec.args.arg.name} (number) (${spec.args.arg.units})`);
expect(input).toHaveAttribute('type', 'text');
});
test('render an unchecked toggle switch for a boolean', async () => {
const spec = baseArgsSpec('boolean');
const { findByLabelText } = renderSetupFromSpec(spec, UI_SPEC);
const { findByLabelText } = renderSetupFromSpec(spec, INPUT_FIELD_ORDER);
const input = await findByLabelText(`${spec.args.arg.name}`);
// for some reason, the type is still checkbox when it renders as a switch
expect(input).toHaveAttribute('type', 'checkbox');
@ -145,7 +143,7 @@ describe('Arguments form input types', () => {
test('render a toggle with a value', async () => {
const spec = baseArgsSpec('boolean');
const { findByLabelText } = renderSetupFromSpec(spec, UI_SPEC, { arg: true });
const { findByLabelText } = renderSetupFromSpec(spec, INPUT_FIELD_ORDER, { arg: true });
const input = await findByLabelText(`${spec.args.arg.name}`);
// for some reason, the type is still checkbox when it renders as a switch
expect(input).toBeChecked();
@ -157,7 +155,7 @@ describe('Arguments form input types', () => {
a: { display_name: 'Option A' },
b: { display_name: 'Option B' },
};
const { findByLabelText } = renderSetupFromSpec(spec, UI_SPEC);
const { findByLabelText } = renderSetupFromSpec(spec, INPUT_FIELD_ORDER);
const input = await findByLabelText(`${spec.args.arg.name}`);
expect(input).toHaveDisplayValue('Option A');
expect(input).toHaveValue('a');
@ -167,7 +165,7 @@ describe('Arguments form input types', () => {
test('render a select input for an option_string list', async () => {
const spec = baseArgsSpec('option_string');
spec.args.arg.options = ['a', 'b'];
const { findByLabelText } = renderSetupFromSpec(spec, UI_SPEC);
const { findByLabelText } = renderSetupFromSpec(spec, INPUT_FIELD_ORDER);
const input = await findByLabelText(`${spec.args.arg.name}`);
expect(input).toHaveValue('a');
expect(input).not.toHaveValue('b');
@ -182,7 +180,7 @@ describe('Arguments form input types', () => {
paramZ: missingValue, // paramZ is not in the ARGS_SPEC
};
const { findByLabelText, queryByText } = renderSetupFromSpec(spec, UI_SPEC, initArgs);
const { findByLabelText, queryByText } = renderSetupFromSpec(spec, INPUT_FIELD_ORDER, initArgs);
const input = await findByLabelText(`${spec.args.arg.name} (number) (${spec.args.arg.units})`);
await waitFor(() => expect(input).toHaveValue(displayedValue));
expect(queryByText(missingValue)).toBeNull();
@ -195,6 +193,7 @@ describe('Arguments form interactions', () => {
[[Object.keys(BASE_MODEL_SPEC.args), VALIDATION_MESSAGE]]
);
fetchArgsEnabled.mockResolvedValue(BASE_ARGS_ENABLED);
getDynamicDropdowns.mockResolvedValue({});
setupOpenExternalUrl();
});
@ -206,7 +205,7 @@ describe('Arguments form interactions', () => {
const spec = baseArgsSpec('csv');
const {
findByRole, findByLabelText,
} = renderSetupFromSpec(spec, UI_SPEC);
} = renderSetupFromSpec(spec, INPUT_FIELD_ORDER);
const input = await findByLabelText((content) => content.startsWith(spec.args.arg.name));
expect(input).toHaveAttribute('type', 'text');
@ -235,7 +234,7 @@ describe('Arguments form interactions', () => {
const spec = baseArgsSpec('csv');
const {
findByRole, findByLabelText,
} = renderSetupFromSpec(spec, UI_SPEC);
} = renderSetupFromSpec(spec, INPUT_FIELD_ORDER);
const filepath = 'grilled_cheese.csv';
const mockDialogData = { filePaths: [filepath] };
@ -253,7 +252,7 @@ describe('Arguments form interactions', () => {
spec.args.arg.required = true;
const {
findByText, findByLabelText, queryByText,
} = renderSetupFromSpec(spec, UI_SPEC);
} = renderSetupFromSpec(spec, INPUT_FIELD_ORDER);
const input = await findByLabelText((content) => content.startsWith(spec.args.arg.name));
@ -282,7 +281,7 @@ describe('Arguments form interactions', () => {
const spy = jest.spyOn(SetupTab.WrappedComponent.prototype, 'investValidate');
const spec = baseArgsSpec('directory');
spec.args.arg.required = true;
const { findByLabelText } = renderSetupFromSpec(spec, UI_SPEC);
const { findByLabelText } = renderSetupFromSpec(spec, INPUT_FIELD_ORDER);
const input = await findByLabelText((content) => content.startsWith(spec.args.arg.name));
spy.mockClear(); // it was already called once on render
@ -298,7 +297,7 @@ describe('Arguments form interactions', () => {
const spy = jest.spyOn(SetupTab.WrappedComponent.prototype, 'investValidate');
const spec = baseArgsSpec('directory');
spec.args.arg.required = true;
const { findByLabelText } = renderSetupFromSpec(spec, UI_SPEC);
const { findByLabelText } = renderSetupFromSpec(spec, INPUT_FIELD_ORDER);
const input = await findByLabelText((content) => content.startsWith(spec.args.arg.name));
spy.mockClear(); // it was already called once on render
@ -316,7 +315,7 @@ describe('Arguments form interactions', () => {
spec.args.arg.required = true;
const {
findByText, findByLabelText, queryByText,
} = renderSetupFromSpec(spec, UI_SPEC);
} = renderSetupFromSpec(spec, INPUT_FIELD_ORDER);
const input = await findByLabelText((content) => content.startsWith(spec.args.arg.name));
expect(input).toHaveClass('is-invalid');
@ -334,7 +333,7 @@ describe('Arguments form interactions', () => {
const spec = baseArgsSpec('csv');
spec.args.arg.required = false;
fetchValidation.mockResolvedValue([]);
const { findByLabelText } = renderSetupFromSpec(spec, UI_SPEC);
const { findByLabelText } = renderSetupFromSpec(spec, INPUT_FIELD_ORDER);
const input = await findByLabelText((content) => content.includes('optional'));
@ -352,7 +351,7 @@ describe('Arguments form interactions', () => {
const spy = jest.spyOn(ipcRenderer, 'send')
.mockImplementation(() => Promise.resolve());
const spec = baseArgsSpec('directory');
const { findByText, findByRole } = renderSetupFromSpec(spec, UI_SPEC);
const { findByText, findByRole } = renderSetupFromSpec(spec, INPUT_FIELD_ORDER);
await userEvent.click(await findByRole('button', { name: /info about/ }));
expect(await findByText(spec.args.arg.about)).toBeInTheDocument();
const link = await findByRole('link', { name: /user guide/ });
@ -396,21 +395,9 @@ describe('UI spec functionality', () => {
};
fetchArgsEnabled.mockResolvedValue({ arg1: false, arg2: true });
const uiSpec = {
order: [Object.keys(spec.args)],
enabledFunctions: {
// enabled if arg1 is sufficient
arg2: ((state) => state.argsEnabled.arg1 && !!state.argsValues.arg1.value),
// enabled if arg1 and arg2 are sufficient
arg3: ((state) => state.argsEnabled.arg1 && !!state.argsValues.arg1.value
&& (state.argsEnabled.arg2 && !!state.argsValues.arg2.value)),
// enabled if arg1 is sufficient and arg2 is not sufficient
arg4: ((state) => state.argsEnabled.arg1 && !!state.argsValues.arg1.value
&& !(state.argsEnabled.arg2 && !!state.argsValues.arg2.value)),
},
};
const inputFieldOrder = [Object.keys(spec.args)];
const { findByLabelText } = renderSetupFromSpec(spec, uiSpec);
const { findByLabelText } = renderSetupFromSpec(spec, inputFieldOrder);
const arg1 = await findByLabelText((content) => content.startsWith(spec.args.arg1.name));
const arg2 = await findByLabelText((content) => content.startsWith(spec.args.arg2.name));
const arg3 = await findByLabelText((content) => content.startsWith(spec.args.arg3.name));
@ -435,18 +422,15 @@ describe('UI spec functionality', () => {
name: 'bfoo',
type: 'option_string',
options: {},
dropdown_function: 'function to retrieve arg1 column names',
},
},
};
const uiSpec = {
order: [Object.keys(spec.args)],
dropdown_functions: {
arg2: 'function to retrieve arg1 column names',
},
};
const inputFieldOrder = [Object.keys(spec.args)];
const {
findByLabelText, findByText, queryByText,
} = renderSetupFromSpec(spec, uiSpec);
} = renderSetupFromSpec(spec, inputFieldOrder);
const arg1 = await findByLabelText((content) => content.startsWith(spec.args.arg1.name));
let option = await queryByText('Field1');
expect(option).toBeNull();
@ -487,12 +471,10 @@ describe('UI spec functionality', () => {
},
};
const uiSpec = {
// intentionally leaving out arg6, it should not be in the setup form
order: [['arg4'], ['arg3', 'arg2'], ['arg1'], ['arg5']],
};
// intentionally leaving out arg6, it should not be in the setup form
const inputFieldOrder = [['arg4'], ['arg3', 'arg2'], ['arg1'], ['arg5']];
const { findByTestId, queryByText } = renderSetupFromSpec(spec, uiSpec);
const { findByTestId, queryByText } = renderSetupFromSpec(spec, inputFieldOrder);
const form = await findByTestId('setup-form');
await waitFor(() => {
@ -534,7 +516,7 @@ describe('Misc form validation stuff', () => {
},
},
};
const uiSpec = { order: [Object.keys(spec.args)] };
const inputFieldOrder = [Object.keys(spec.args)];
// Mocking to return the payload so we can assert we always send
// correct payload to this endpoint.
@ -543,7 +525,7 @@ describe('Misc form validation stuff', () => {
);
fetchArgsEnabled.mockResolvedValue({ a: true, b: true, c: true });
renderSetupFromSpec(spec, uiSpec);
renderSetupFromSpec(spec, inputFieldOrder);
await waitFor(() => {
const expectedKeys = ['model_id', 'args'];
const payload = fetchValidation.mock.results[0].value;
@ -566,7 +548,7 @@ describe('Misc form validation stuff', () => {
},
},
};
const uiSpec = { order: [Object.keys(spec.args)] };
const inputFieldOrder =[Object.keys(spec.args)];
const vectorValue = './vector.shp';
const expectedVal1 = '-84.9';
const vectorBox = `[${expectedVal1}, 19.1, -69.1, 29.5]`;
@ -580,7 +562,7 @@ describe('Misc form validation stuff', () => {
fetchValidation.mockResolvedValue([[Object.keys(spec.args), message]]);
fetchArgsEnabled.mockResolvedValue({ vector: true, raster: true });
const { findByLabelText } = renderSetupFromSpec(spec, uiSpec);
const { findByLabelText } = renderSetupFromSpec(spec, inputFieldOrder);
const vectorInput = await findByLabelText((content) => content.startsWith(spec.args.vector.name));
const rasterInput = await findByLabelText(RegExp(`^${spec.args.raster.name}`));
await userEvent.type(vectorInput, vectorValue);
@ -621,7 +603,7 @@ describe('Form drag-and-drop', () => {
},
},
};
const uiSpec = { order: [Object.keys(spec.args)] };
const inputFieldOrder = [Object.keys(spec.args)];
fetchValidation.mockResolvedValue(
[[Object.keys(spec.args), VALIDATION_MESSAGE]]
);
@ -638,7 +620,7 @@ describe('Form drag-and-drop', () => {
const {
findByLabelText, findByTestId,
} = renderSetupFromSpec(spec, uiSpec);
} = renderSetupFromSpec(spec, inputFieldOrder);
const setupForm = await findByTestId('setup-form');
// This should work but doesn't due to lack of dataTransfer object in jsdom:
@ -684,7 +666,7 @@ describe('Form drag-and-drop', () => {
},
},
};
const uiSpec = { order: [Object.keys(spec.args)] };
const inputFieldOrder = [Object.keys(spec.args)];
fetchValidation.mockResolvedValue(
[[Object.keys(spec.args), VALIDATION_MESSAGE]]
);
@ -701,7 +683,7 @@ describe('Form drag-and-drop', () => {
const {
findByLabelText, findByTestId,
} = renderSetupFromSpec(spec, uiSpec);
} = renderSetupFromSpec(spec, inputFieldOrder);
const setupForm = await findByTestId('setup-form');
const fileDragEvent = createEvent.dragEnter(setupForm);
@ -747,13 +729,13 @@ describe('Form drag-and-drop', () => {
},
},
};
const uiSpec = { order: [Object.keys(spec.args)] };
const inputFieldOrder = [Object.keys(spec.args)];
fetchValidation.mockResolvedValue(
[[Object.keys(spec.args), VALIDATION_MESSAGE]]
);
fetchArgsEnabled.mockResolvedValue({ arg1: true, arg2: true });
const { findByTestId } = renderSetupFromSpec(spec, uiSpec);
const { findByTestId } = renderSetupFromSpec(spec, inputFieldOrder);
const setupForm = await findByTestId('setup-form');
const fileDragEnterEvent = createEvent.dragEnter(setupForm);
@ -792,7 +774,7 @@ describe('Form drag-and-drop', () => {
},
},
};
const uiSpec = { order: [Object.keys(spec.args)] };
const inputFieldOrder = [Object.keys(spec.args)];
fetchValidation.mockResolvedValue(
[[Object.keys(spec.args), VALIDATION_MESSAGE]]
);
@ -800,7 +782,7 @@ describe('Form drag-and-drop', () => {
const {
findByLabelText, findByTestId,
} = renderSetupFromSpec(spec, uiSpec);
} = renderSetupFromSpec(spec, inputFieldOrder);
const setupForm = await findByTestId('setup-form');
const setupInput = await findByLabelText((content) => content.startsWith(spec.args.arg1.name));
@ -846,13 +828,13 @@ describe('Form drag-and-drop', () => {
},
},
};
const uiSpec = { order: [Object.keys(spec.args)] };
const inputFieldOrder = [Object.keys(spec.args)];
fetchValidation.mockResolvedValue(
[[Object.keys(spec.args), VALIDATION_MESSAGE]]
);
fetchArgsEnabled.mockResolvedValue({ arg1: true, arg2: true });
const { findByLabelText } = renderSetupFromSpec(spec, uiSpec);
const { findByLabelText } = renderSetupFromSpec(spec, inputFieldOrder);
const setupInput = await findByLabelText((content) => content.startsWith(spec.args.arg1.name));
const fileDragEnterEvent = createEvent.dragEnter(setupInput);
@ -891,22 +873,18 @@ describe('Form drag-and-drop', () => {
arg2: {
name: 'AOI',
type: 'vector',
enabled: false,
},
},
};
const uiSpec = {
order: [Object.keys(spec.args)],
enabledFunctions: {
arg2: (() => false), // make this arg always disabled
},
};
const inputFieldOrder = [Object.keys(spec.args)];
fetchValidation.mockResolvedValue(
[[Object.keys(spec.args), VALIDATION_MESSAGE]]
);
fetchArgsEnabled.mockResolvedValue({ arg1: true, arg2: false });
const { findByLabelText } = renderSetupFromSpec(spec, uiSpec);
const { findByLabelText } = renderSetupFromSpec(spec, inputFieldOrder);
const setupInput = await findByLabelText((content) => content.startsWith(spec.args.arg2.name));
const fileDragEnterEvent = createEvent.dragEnter(setupInput);