From d14662f1a4c4d644e1f5e7e305d5f3565bedf331 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96ner=20Zafer?= Date: Wed, 16 Jun 2021 11:21:40 +0200 Subject: [PATCH 1/2] Integrate (ui) schema based control enablement The JSON Forms core utilities will now also consider the JSON Schema 'readOnly' property as well as 'readonly' and 'readOnly' properties defined in ui schemas. The algorithm checks in the following order whether the form wide readonly flag is set, an enablement rule is defined for the ui schema element, ui schema or config options are set and whether the JSON Schema specifies 'readOnly'. If none of them apply the enablement of the parent is used. Includes example and test cases. --- packages/angular/src/abstract-control.ts | 5 +- packages/core/src/util/cell.ts | 7 +- packages/core/src/util/renderer.ts | 48 +++--- packages/core/src/util/runtime.ts | 38 +++++ packages/core/test/util/cell.test.ts | 5 +- packages/core/test/util/runtime.test.ts | 206 ++++++++++++++++++++++- packages/examples/src/index.ts | 4 +- packages/examples/src/readonly.ts | 81 +++++++++ 8 files changed, 361 insertions(+), 33 deletions(-) create mode 100644 packages/examples/src/readonly.ts diff --git a/packages/angular/src/abstract-control.ts b/packages/angular/src/abstract-control.ts index 02a611a27..e4eac9ec4 100644 --- a/packages/angular/src/abstract-control.ts +++ b/packages/angular/src/abstract-control.ts @@ -148,10 +148,7 @@ export abstract class JsonFormsAbstractControl< } isEnabled(): boolean { - return ( - this.enabled && - (this.uischema.options ? !this.uischema.options.readonly : true) - ); + return this.enabled; } protected getOwnProps(): OwnPropsOfControl { diff --git a/packages/core/src/util/cell.ts b/packages/core/src/util/cell.ts index 31a6c0148..4629f5b15 100644 --- a/packages/core/src/util/cell.ts +++ b/packages/core/src/util/cell.ts @@ -40,7 +40,7 @@ import { DispatchPropsOfControl, EnumOption, enumToEnumOptionMapper, - mapDispatchToControlProps, + mapDispatchToControlProps } from './renderer'; import { JsonFormsState } from '../store'; import { JsonFormsCellRendererRegistryEntry } from '../reducers/cells'; @@ -111,7 +111,8 @@ export const mapStateToCellProps = ( : isVisible(uischema, rootData, undefined, getAjv(state)); const readonly = state.jsonforms.readonly; const enabled = - !readonly && (ownProps.enabled !== undefined + !readonly && + (ownProps.enabled !== undefined ? ownProps.enabled : isEnabled(uischema, rootData, undefined, getAjv(state))); const errors = formatErrorMessage( @@ -166,7 +167,7 @@ export const defaultMapStateToEnumCellProps = ( const options: EnumOption[] = ownProps.options || props.schema.enum?.map(enumToEnumOptionMapper) || - props.schema.const && [enumToEnumOptionMapper(props.schema.const)]; + (props.schema.const && [enumToEnumOptionMapper(props.schema.const)]); return { ...props, options diff --git a/packages/core/src/util/renderer.ts b/packages/core/src/util/renderer.ts index e58334e45..0f4343e4d 100644 --- a/packages/core/src/util/renderer.ts +++ b/packages/core/src/util/renderer.ts @@ -48,9 +48,8 @@ import { createLabelDescriptionFrom, Dispatch, formatErrorMessage, - hasEnableRule, hasShowRule, - isEnabled, + isInherentlyEnabled, isVisible, moveDown, moveUp, @@ -405,11 +404,6 @@ export const mapStateToControlProps = ( ownProps.visible === undefined || hasShowRule(uischema) ? isVisible(uischema, rootData, ownProps.path, getAjv(state)) : ownProps.visible; - const readonly = state.jsonforms.readonly; - const enabled: boolean = - !readonly && (ownProps.enabled === undefined || hasEnableRule(uischema) - ? isEnabled(uischema, rootData, ownProps.path, getAjv(state) ) - : ownProps.enabled); const controlElement = uischema as ControlElement; const id = ownProps.id; const rootSchema = getSchema(state); @@ -429,6 +423,15 @@ export const mapStateToControlProps = ( const data = Resolve.data(rootData, path); const labelDesc = createLabelDescriptionFrom(uischema, resolvedSchema); const label = labelDesc.show ? labelDesc.text : ''; + const config = getConfig(state); + const enabled: boolean = isInherentlyEnabled( + state, + ownProps, + uischema, + resolvedSchema || rootSchema, + rootData, + config + ); return { data, description, @@ -474,9 +477,9 @@ export const mapStateToEnumControlProps = ( ): StatePropsOfControl & OwnPropsOfEnum => { const props: StatePropsOfControl = mapStateToControlProps(state, ownProps); const options: EnumOption[] = - ownProps.options || - props.schema.enum?.map(enumToEnumOptionMapper) || - props.schema.const && [enumToEnumOptionMapper(props.schema.const)]; + ownProps.options || + props.schema.enum?.map(enumToEnumOptionMapper) || + (props.schema.const && [enumToEnumOptionMapper(props.schema.const)]); return { ...props, options @@ -517,7 +520,8 @@ export const mapStateToMultiEnumControlProps = ( const items = props.schema.items as JsonSchema; const options: EnumOption[] = ownProps.options || - (items?.oneOf && (items.oneOf as JsonSchema[]).map(oneOfToEnumOptionMapper)) || + (items?.oneOf && + (items.oneOf as JsonSchema[]).map(oneOfToEnumOptionMapper)) || items?.enum?.map(enumToEnumOptionMapper); return { ...props, @@ -700,14 +704,14 @@ export const mapDispatchToArrayControlProps = ( }); export interface DispatchPropsOfMultiEnumControl { - addItem: (path:string, value: any) => void; - removeItem? :(path:string, toDelete: any) => void; + addItem: (path: string, value: any) => void; + removeItem?: (path: string, toDelete: any) => void; } export const mapDispatchToMultiEnumProps = ( dispatch: Dispatch ): DispatchPropsOfMultiEnumControl => ({ - addItem: (path:string, value: any) => { + addItem: (path: string, value: any) => { dispatch( update(path, data => { if (data === undefined || data === null) { @@ -718,7 +722,7 @@ export const mapDispatchToMultiEnumProps = ( }) ); }, - removeItem: (path:string, toDelete: any) => { + removeItem: (path: string, toDelete: any) => { dispatch( update(path, data => { const indexInData = data.indexOf(toDelete); @@ -774,13 +778,17 @@ export const mapStateToLayoutProps = ( ownProps.visible === undefined || hasShowRule(uischema) ? isVisible(ownProps.uischema, rootData, ownProps.path, getAjv(state)) : ownProps.visible; - const readonly = state.jsonforms.readonly; - const enabled: boolean = - !readonly && (ownProps.enabled === undefined || hasEnableRule(uischema) - ? isEnabled(ownProps.uischema, rootData, ownProps.path, getAjv(state)) - : ownProps.enabled); const data = Resolve.data(rootData, ownProps.path); + const config = getConfig(state); + const enabled: boolean = isInherentlyEnabled( + state, + ownProps, + uischema, + ownProps.schema, + rootData, + config + ); return { ...layoutDefaultProps, diff --git a/packages/core/src/util/runtime.ts b/packages/core/src/util/runtime.ts index 11f2908a3..7cd87508f 100644 --- a/packages/core/src/util/runtime.ts +++ b/packages/core/src/util/runtime.ts @@ -36,6 +36,9 @@ import { import { resolveData } from './resolvers'; import { composeWithUi } from './path'; import { Ajv } from 'ajv'; +import { getAjv } from '../reducers'; +import { JsonFormsState } from '../store'; +import { JsonSchema } from '../models/jsonSchema'; const isOrCondition = (condition: Condition): condition is OrCondition => condition.type === 'OR'; @@ -177,3 +180,38 @@ export const isEnabled = ( return true; }; + +export const isInherentlyEnabled = ( + state: JsonFormsState, + ownProps: any, + uischema: UISchemaElement, + schema: JsonSchema & { readOnly?: boolean }, + rootData: any, + config: any +) => { + if (state?.jsonforms?.readonly) { + return false; + } + if (uischema && hasEnableRule(uischema)) { + return isEnabled(uischema, rootData, ownProps?.path, getAjv(state)); + } + if (typeof uischema?.options?.readonly === 'boolean') { + return !uischema.options.readonly; + } + if (typeof uischema?.options?.readOnly === 'boolean') { + return !uischema.options.readOnly; + } + if (typeof config?.readonly === 'boolean') { + return !config.readonly; + } + if (typeof config?.readOnly === 'boolean') { + return !config.readOnly; + } + if (typeof schema?.readOnly === 'boolean') { + return !schema.readOnly; + } + if (typeof ownProps?.enabled === 'boolean') { + return ownProps.enabled; + } + return true; +}; diff --git a/packages/core/test/util/cell.test.ts b/packages/core/test/util/cell.test.ts index c2d06c9c8..841db99f9 100644 --- a/packages/core/test/util/cell.test.ts +++ b/packages/core/test/util/cell.test.ts @@ -284,7 +284,10 @@ test('mapStateToEnumCellProps - set default options for dropdown list', t => { }; const props = defaultMapStateToEnumCellProps(createState(uischema), ownProps); - t.deepEqual(props.options, ['DE', 'IT', 'JP', 'US', 'RU', 'Other'].map(enumToEnumOptionMapper)); + t.deepEqual( + props.options, + ['DE', 'IT', 'JP', 'US', 'RU', 'Other'].map(enumToEnumOptionMapper) + ); t.is(props.data, undefined); }); diff --git a/packages/core/test/util/runtime.test.ts b/packages/core/test/util/runtime.test.ts index 15f9559f6..a7a359e41 100644 --- a/packages/core/test/util/runtime.test.ts +++ b/packages/core/test/util/runtime.test.ts @@ -27,6 +27,8 @@ import { AndCondition, ControlElement, createAjv, + isInherentlyEnabled, + JsonFormsCore, LeafCondition, OrCondition, RuleEffect, @@ -221,8 +223,24 @@ test('evalVisibility show valid case based on schema condition and enum', t => { ruleValue: 'bar' }; t.is(evalVisibility(uischema, data, undefined, createAjv()), true); - t.is(evalVisibility(uischema, { ...data, ruleValue: 'baz' }, undefined, createAjv()), true); - t.is(evalVisibility(uischema, { ...data, ruleValue: 'foo' }, undefined, createAjv()), false); + t.is( + evalVisibility( + uischema, + { ...data, ruleValue: 'baz' }, + undefined, + createAjv() + ), + true + ); + t.is( + evalVisibility( + uischema, + { ...data, ruleValue: 'foo' }, + undefined, + createAjv() + ), + false + ); }); test('evalVisibility show invalid case', t => { @@ -514,6 +532,186 @@ test('evalEnablement disable invalid case based on schema condition', t => { ruleValue: 'bar' }; t.is(evalEnablement(uischema, data, undefined, createAjv()), false); - t.is(evalEnablement(uischema, { ...data, ruleValue: 'baz' }, undefined, createAjv()), false); - t.is(evalEnablement(uischema, { ...data, ruleValue: 'foo' }, undefined, createAjv()), true); + t.is( + evalEnablement( + uischema, + { ...data, ruleValue: 'baz' }, + undefined, + createAjv() + ), + false + ); + t.is( + evalEnablement( + uischema, + { ...data, ruleValue: 'foo' }, + undefined, + createAjv() + ), + true + ); +}); + +test('isInherentlyEnabled disabled globally', t => { + t.false( + isInherentlyEnabled( + { jsonforms: { readonly: true } }, + null, + null, + null, + null, + null + ) + ); +}); + +test('isInherentlyEnabled disabled by ownProps', t => { + t.false( + isInherentlyEnabled(null, { enabled: false }, null, null, null, null) + ); +}); + +test('isInherentlyEnabled enabled by ownProps', t => { + t.true(isInherentlyEnabled(null, { enabled: true }, null, null, null, null)); +}); + +test('isInherentlyEnabled disabled by uischema', t => { + t.false( + isInherentlyEnabled( + null, + null, + ({ options: { readonly: true } } as unknown) as ControlElement, + null, + null, + null + ) + ); +}); + +test('isInherentlyEnabled disabled by uischema over ownProps', t => { + t.false( + isInherentlyEnabled( + null, + { enabled: false }, + ({ options: { readonly: true } } as unknown) as ControlElement, + null, + null, + null + ) + ); +}); + +test('isInherentlyEnabled disabled by uischema over schema', t => { + t.false( + isInherentlyEnabled( + null, + null, + ({ options: { readonly: true } } as unknown) as ControlElement, + { readOnly: false }, + null, + null + ) + ); +}); + +test('isInherentlyEnabled disabled by schema', t => { + t.false( + isInherentlyEnabled(null, null, null, { readOnly: true }, null, null) + ); +}); + +test('isInherentlyEnabled disabled by schema over ownProps', t => { + t.false( + isInherentlyEnabled( + null, + { enabled: true }, + null, + { readOnly: true }, + null, + null + ) + ); +}); + +test('isInherentlyEnabled disabled by rule', t => { + const leafCondition: SchemaBasedCondition = { + scope: '#/properties/ruleValue', + schema: { type: 'string', pattern: 'bar' } + }; + const uischema: ControlElement = { + type: 'Control', + scope: '#/properties/value', + rule: { + effect: RuleEffect.DISABLE, + condition: leafCondition + } + }; + const data = { + value: 'foo', + ruleValue: 'bar' + }; + + t.false( + isInherentlyEnabled( + { jsonforms: { core: { ajv: createAjv() } as JsonFormsCore } }, + null, + uischema, + null, + data, + null + ) + ); +}); + +test('isInherentlyEnabled disabled by global over rule ', t => { + const leafCondition: SchemaBasedCondition = { + scope: '#/properties/ruleValue', + schema: { type: 'string', pattern: 'bar' } + }; + const uischema: ControlElement = { + type: 'Control', + scope: '#/properties/value', + rule: { + effect: RuleEffect.ENABLE, + condition: leafCondition + } + }; + const data = { + value: 'foo', + ruleValue: 'bar' + }; + + t.false( + isInherentlyEnabled( + { + jsonforms: { + readonly: true, + core: { ajv: createAjv() } as JsonFormsCore + } + }, + null, + uischema, + null, + data, + null + ) + ); +}); + +test('isInherentlyEnabled disabled by config', t => { + t.false( + isInherentlyEnabled(null, null, null, null, null, { readonly: true }) + ); +}); + +test('isInherentlyEnabled enabled by config over ownProps', t => { + t.true( + isInherentlyEnabled(null, { enabled: false }, null, null, null, { + readonly: false + }) + ); +}); + +test('isInherentlyEnabled enabled', t => { + t.true(isInherentlyEnabled(null, null, null, null, null, null)); }); diff --git a/packages/examples/src/index.ts b/packages/examples/src/index.ts index 4527bdc9c..49b3662a2 100644 --- a/packages/examples/src/index.ts +++ b/packages/examples/src/index.ts @@ -72,6 +72,7 @@ import * as enumExample from './enum'; import * as radioGroupExample from './radioGroup'; import * as booleanToggle from './booleanToggle'; import * as multiEnum from './multi-enum'; +import * as readonly from './readonly'; export * from './register'; export * from './example'; @@ -128,5 +129,6 @@ export { enumExample, radioGroupExample, booleanToggle, - multiEnum + multiEnum, + readonly }; diff --git a/packages/examples/src/readonly.ts b/packages/examples/src/readonly.ts new file mode 100644 index 000000000..96321c9ad --- /dev/null +++ b/packages/examples/src/readonly.ts @@ -0,0 +1,81 @@ +/* + The MIT License + + Copyright (c) 2017-2019 EclipseSource Munich + https://github.com/eclipsesource/jsonforms + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. +*/ +import { registerExamples } from './register'; + +export const schema = { + type: 'object', + properties: { + readonly: { + type: 'string', + readOnly: true + }, + readonlyByUISchema: { + type: 'string' + }, + notReadonly: { + type: 'string' + } + } +}; + +export const uischema = { + type: 'VerticalLayout', + elements: [ + { + type: 'Control', + scope: '#/properties/readonly', + label: 'A readonly field' + }, + { + type: 'Control', + scope: '#/properties/readonlyByUISchema', + label: 'A readonly field by ui schema', + options: { + readonly: true + } + }, + { + type: 'Control', + scope: '#/properties/notReadonly', + label: 'A normal field' + } + ] +}; + +export const data = { + readonly: 'readonly by schema', + readonlyByUISchema: 'readonly by ui schema', + notReadonly: 'normal field' +}; + +registerExamples([ + { + name: 'Readonly Fields', + label: 'Readonly examples', + data, + schema, + uischema + } +]); From e7d9fa29c60c5ed102eaacad3ec9f6da91226dd9 Mon Sep 17 00:00:00 2001 From: Stefan Dirix Date: Wed, 23 Jun 2021 17:44:45 +0200 Subject: [PATCH 2/2] Update cell enablement algorithm Update cell `enabled` determination to the new enablement algorithm. Note that the cell `enabled` still prefers `ownProps` (except for the form wide readonly flag) over any other determination for performance gains. The Table (or other renderers dispatching to cells) usually already determined whether the cell shall be enabled and therefore hand the correct `enabled` prop over. If that is not wanted, the new enablement algorithm will kick in when not setting this prop. Also adds more `isInherentlyEnabled` test cases and fixes the JSON Schema `readOnly` lookup. --- packages/core/src/util/cell.ts | 55 +++++++++++++-------- packages/core/src/util/renderer.ts | 2 +- packages/core/src/util/runtime.ts | 11 +++-- packages/core/test/util/runtime.test.ts | 64 ++++++++++++++++++++++++- 4 files changed, 107 insertions(+), 25 deletions(-) diff --git a/packages/core/src/util/cell.ts b/packages/core/src/util/cell.ts index 4629f5b15..3a1c518ce 100644 --- a/packages/core/src/util/cell.ts +++ b/packages/core/src/util/cell.ts @@ -25,26 +25,20 @@ import isEmpty from 'lodash/isEmpty'; import union from 'lodash/union'; import { getConfig, getData, getErrorAt, getSchema, getAjv } from '../reducers'; -import { - AnyAction, - Dispatch, - formatErrorMessage, - isEnabled, - isVisible, - OwnPropsOfControl, - OwnPropsOfEnum, - Resolve, - StatePropsOfScopedRenderer -} from '.'; import { DispatchPropsOfControl, EnumOption, enumToEnumOptionMapper, - mapDispatchToControlProps + mapDispatchToControlProps, + OwnPropsOfControl, + OwnPropsOfEnum, + StatePropsOfScopedRenderer } from './renderer'; import { JsonFormsState } from '../store'; import { JsonFormsCellRendererRegistryEntry } from '../reducers/cells'; -import { JsonSchema } from '..'; +import { JsonSchema } from '../models/jsonSchema'; +import { isInherentlyEnabled, isVisible } from './runtime'; +import { AnyAction, Dispatch, formatErrorMessage, Resolve } from '.'; export { JsonFormsCellRendererRegistryEntry }; @@ -109,17 +103,38 @@ export const mapStateToCellProps = ( ownProps.visible !== undefined ? ownProps.visible : isVisible(uischema, rootData, undefined, getAjv(state)); - const readonly = state.jsonforms.readonly; - const enabled = - !readonly && - (ownProps.enabled !== undefined - ? ownProps.enabled - : isEnabled(uischema, rootData, undefined, getAjv(state))); + + const rootSchema = getSchema(state); + const config = getConfig(state); + + /* When determining the enabled state of cells we take a shortcut: At the + * moment it's only possible to configure enablement and disablement at the + * control level. Therefore the renderer using the cell, for example a + * table renderer, determines whether a cell is enabled and should hand + * over the prop themselves. If that prop was given, we prefer it over + * anything else to save evaluation effort (except for the global readonly + * flag). For example it would be quite expensive to evaluate the same ui schema + * rule again and again for each cell of a table. */ + let enabled; + if (state.jsonforms.readonly === true) { + enabled = false; + } else if (typeof ownProps.enabled === 'boolean') { + enabled = ownProps.enabled; + } else { + enabled = isInherentlyEnabled( + state, + ownProps, + uischema, + schema || rootSchema, + rootData, + config + ); + } + const errors = formatErrorMessage( union(getErrorAt(path, schema)(state).map(error => error.message)) ); const isValid = isEmpty(errors); - const rootSchema = getSchema(state); return { data: Resolve.data(rootData, path), diff --git a/packages/core/src/util/renderer.ts b/packages/core/src/util/renderer.ts index 0f4343e4d..f6e2c5d08 100644 --- a/packages/core/src/util/renderer.ts +++ b/packages/core/src/util/renderer.ts @@ -785,7 +785,7 @@ export const mapStateToLayoutProps = ( state, ownProps, uischema, - ownProps.schema, + undefined, // layouts have no associated schema rootData, config ); diff --git a/packages/core/src/util/runtime.ts b/packages/core/src/util/runtime.ts index 7cd87508f..6160ede16 100644 --- a/packages/core/src/util/runtime.ts +++ b/packages/core/src/util/runtime.ts @@ -181,11 +181,16 @@ export const isEnabled = ( return true; }; +/** + * Indicates whether the given `uischema` element shall be enabled or disabled. + * Checks the global readonly flag, uischema rule, uischema options (including the config), + * the schema and the enablement indicator of the parent. + */ export const isInherentlyEnabled = ( state: JsonFormsState, ownProps: any, uischema: UISchemaElement, - schema: JsonSchema & { readOnly?: boolean }, + schema: JsonSchema & { readOnly?: boolean } | undefined, rootData: any, config: any ) => { @@ -207,8 +212,8 @@ export const isInherentlyEnabled = ( if (typeof config?.readOnly === 'boolean') { return !config.readOnly; } - if (typeof schema?.readOnly === 'boolean') { - return !schema.readOnly; + if (schema?.readOnly === true) { + return false; } if (typeof ownProps?.enabled === 'boolean') { return ownProps.enabled; diff --git a/packages/core/test/util/runtime.test.ts b/packages/core/test/util/runtime.test.ts index a7a359e41..19367c22f 100644 --- a/packages/core/test/util/runtime.test.ts +++ b/packages/core/test/util/runtime.test.ts @@ -592,7 +592,7 @@ test('isInherentlyEnabled disabled by uischema over ownProps', t => { t.false( isInherentlyEnabled( null, - { enabled: false }, + { enabled: true }, ({ options: { readonly: true } } as unknown) as ControlElement, null, null, @@ -601,6 +601,32 @@ test('isInherentlyEnabled disabled by uischema over ownProps', t => { ); }); +test('isInherentlyEnabled enabled by uischema over schema', t => { + t.true( + isInherentlyEnabled( + null, + null, + ({ options: { readonly: false } } as unknown) as ControlElement, + { readOnly: true }, + null, + null + ) + ); +}); + +test('isInherentlyEnabled disabled by ownProps over schema enablement', t => { + t.false( + isInherentlyEnabled( + null, + { enabled: false}, + null, + { readOnly: false }, + null, + null + ) + ); +}); + test('isInherentlyEnabled disabled by uischema over schema', t => { t.false( isInherentlyEnabled( @@ -712,6 +738,42 @@ test('isInherentlyEnabled enabled by config over ownProps', t => { ); }); +test('isInherentlyEnabled enabled by uischema over config', t => { + t.true( + isInherentlyEnabled( + null, + null, + ({ options: { readonly: false } } as unknown) as ControlElement, + null, + null, + { readonly: true } + ) + ); +}); + +test('isInherentlyEnabled prefer readonly over readOnly', t => { + t.true( + isInherentlyEnabled( + null, + null, + ({ options: { readonly: false, readOnly: true } } as unknown) as ControlElement, + null, + null, + null + ) + ); + t.false( + isInherentlyEnabled( + null, + null, + ({ options: { readonly: true, readOnly: false } } as unknown) as ControlElement, + null, + null, + null + ) + ); +}); + test('isInherentlyEnabled enabled', t => { t.true(isInherentlyEnabled(null, null, null, null, null, null)); });