diff --git a/packages/angular-material/.eslintrc.js b/packages/angular-material/.eslintrc.js index 2b67856c2..486c568b6 100644 --- a/packages/angular-material/.eslintrc.js +++ b/packages/angular-material/.eslintrc.js @@ -9,45 +9,54 @@ module.exports = { }, // There is no file include in ESLint. Thus, ignore all and include files via negative ignore (!) ignorePatterns: ['/*', '!/src', '!/test', '!/example', '/example/dist'], - extends: [ - 'eslint:recommended', - 'plugin:@typescript-eslint/recommended', - 'plugin:import/recommended', - 'plugin:import/typescript', - 'plugin:@angular-eslint/recommended', - 'plugin:@angular-eslint/template/process-inline-templates', - 'plugin:prettier/recommended', - ], - rules: { - '@angular-eslint/component-class-suffix': 'off', - '@angular-eslint/directive-class-suffix': 'off', - '@angular-eslint/no-conflicting-lifecycle': 'warn', - '@typescript-eslint/no-explicit-any': 'off', - // Base rule must be disabled to avoid incorrect errors - 'no-unused-vars': 'off', - '@typescript-eslint/no-unused-vars': [ - 'warn', // or "error" - { - argsIgnorePattern: '^_', - varsIgnorePattern: '^_', - caughtErrorsIgnorePattern: '^_', - }, - ], - // workaround for - // https://github.com/import-js/eslint-plugin-import/issues/1810: - 'import/no-unresolved': [ - 'error', - { - ignore: [ - '@angular/cdk/.*', - '@angular/core/.*', - '@angular/material/.*', - '@angular/platform-browser/.*', - '@angular/platform-browser-dynamic/.*', - 'core-js/es7/.*', - 'zone.js/.*', + overrides: [ + { + files: ['*.ts'], + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:import/recommended', + 'plugin:import/typescript', + 'plugin:@angular-eslint/recommended', + 'plugin:@angular-eslint/template/process-inline-templates', + 'plugin:prettier/recommended', + ], + rules: { + '@angular-eslint/component-class-suffix': 'off', + '@angular-eslint/directive-class-suffix': 'off', + '@angular-eslint/no-conflicting-lifecycle': 'warn', + '@typescript-eslint/no-explicit-any': 'off', + // Base rule must be disabled to avoid incorrect errors + 'no-unused-vars': 'off', + '@typescript-eslint/no-unused-vars': [ + 'warn', // or "error" + { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + caughtErrorsIgnorePattern: '^_', + }, + ], + // workaround for + // https://github.com/import-js/eslint-plugin-import/issues/1810: + 'import/no-unresolved': [ + 'error', + { + ignore: [ + '@angular/cdk/.*', + '@angular/core/.*', + '@angular/material/.*', + '@angular/platform-browser/.*', + '@angular/platform-browser-dynamic/.*', + 'core-js/es7/.*', + 'zone.js/.*', + ], + }, ], }, - ], - }, + }, + { + files: '*.html', + extends: ['plugin:@angular-eslint/template/recommended'], + } + ], }; diff --git a/packages/angular-material/src/library/controls/enum.renderer.html b/packages/angular-material/src/library/controls/enum.renderer.html new file mode 100644 index 000000000..756c341c0 --- /dev/null +++ b/packages/angular-material/src/library/controls/enum.renderer.html @@ -0,0 +1,30 @@ + + {{ label }} + + + @for (option of filteredOptions | async; track option.value) { + + {{ option.label }} + + } + + {{ + description + }} + {{ error }} + diff --git a/packages/angular-material/src/library/controls/enum.renderer.scss b/packages/angular-material/src/library/controls/enum.renderer.scss new file mode 100644 index 000000000..9da6d18ac --- /dev/null +++ b/packages/angular-material/src/library/controls/enum.renderer.scss @@ -0,0 +1,7 @@ +:host { + display: flex; + flex-direction: row; +} +mat-form-field { + flex: 1 1 auto; +} diff --git a/packages/angular-material/src/library/controls/autocomplete.renderer.ts b/packages/angular-material/src/library/controls/enum.renderer.ts similarity index 74% rename from packages/angular-material/src/library/controls/autocomplete.renderer.ts rename to packages/angular-material/src/library/controls/enum.renderer.ts index 32e19437e..b5de1b13a 100644 --- a/packages/angular-material/src/library/controls/autocomplete.renderer.ts +++ b/packages/angular-material/src/library/controls/enum.renderer.ts @@ -36,8 +36,10 @@ import { ControlElement, EnumOption, isEnumControl, + isOneOfEnumControl, JsonFormsState, mapStateToEnumControlProps, + mapStateToOneOfEnumControlProps, OwnPropsOfControl, OwnPropsOfEnum, RankedTester, @@ -53,50 +55,9 @@ import { MatInputModule } from '@angular/material/input'; import { MatAutocompleteModule } from '@angular/material/autocomplete'; @Component({ - selector: 'AutocompleteControlRenderer', - template: ` - - {{ label }} - - - @for (option of filteredOptions | async; track option.value) { - - {{ option.label }} - - } - - {{ - description - }} - {{ error }} - - `, - styles: [ - ` - :host { - display: flex; - flex-direction: row; - } - mat-form-field { - flex: 1 1 auto; - } - `, - ], + selector: 'OneOfEnumControlRenderer', + templateUrl: './enum.renderer.html', + styleUrls: ['./enum.renderer.scss'], changeDetection: ChangeDetectionStrategy.OnPush, imports: [ CommonModule, @@ -106,11 +67,11 @@ import { MatAutocompleteModule } from '@angular/material/autocomplete'; MatAutocompleteModule, ], }) -export class AutocompleteControlRenderer +export class OneOfEnumControlRenderer extends JsonFormsControl implements OnInit { - @Input() options?: EnumOption[] | string[]; + @Input() options?: EnumOption[]; valuesToTranslatedOptions?: Map; filteredOptions: Observable; shouldFilter: boolean; @@ -123,7 +84,7 @@ export class AutocompleteControlRenderer protected override mapToProps( state: JsonFormsState ): StatePropsOfControl & OwnPropsOfEnum { - return mapStateToEnumControlProps(state, this.getOwnProps()); + return mapStateToOneOfEnumControlProps(state, this.getOwnProps()); } getEventValue = (event: any) => event.target.value; @@ -209,29 +170,51 @@ export class AutocompleteControlRenderer protected getOwnProps(): OwnPropsOfControl & OwnPropsOfEnum { return { ...super.getOwnProps(), - options: this.stringOptionsToEnumOptions(this.options), + options: this.options, }; } +} - /** - * For {@link options} input backwards compatibility - */ - protected stringOptionsToEnumOptions( - options: typeof this.options - ): EnumOption[] | undefined { - if (!options) { - return undefined; - } +export const oneOfEnumControlTester: RankedTester = rankWith( + 5, + isOneOfEnumControl +); - return options.every((item) => typeof item === 'string') - ? options.map((str) => { - return { - label: str, - value: str, - } satisfies EnumOption; - }) - : options; +@Component({ + selector: 'EnumControlRenderer, AutocompleteControlRenderer', + templateUrl: './enum.renderer.html', + styleUrls: ['./enum.renderer.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ + CommonModule, + ReactiveFormsModule, + MatFormFieldModule, + MatInputModule, + MatAutocompleteModule, + ], +}) +export class EnumControlRenderer extends OneOfEnumControlRenderer { + // eslint-disable-next-line @angular-eslint/no-input-rename + @Input('options') + set stringOptions(strOptions: string[]) { + this.options = strOptions.map((str) => { + return { + label: str, + value: str, + }; + }); + } + + protected override mapToProps( + state: JsonFormsState + ): StatePropsOfControl & OwnPropsOfEnum { + return mapStateToEnumControlProps(state, this.getOwnProps()); } } +/** + * For {@link AutocompleteControlRenderer} class name backwards compatibility + */ +export { EnumControlRenderer as AutocompleteControlRenderer }; + export const enumControlTester: RankedTester = rankWith(2, isEnumControl); diff --git a/packages/angular-material/src/library/controls/index.ts b/packages/angular-material/src/library/controls/index.ts index ec7666823..2a721313d 100644 --- a/packages/angular-material/src/library/controls/index.ts +++ b/packages/angular-material/src/library/controls/index.ts @@ -29,4 +29,4 @@ export * from './number.renderer'; export * from './range.renderer'; export * from './date.renderer'; export * from './toggle.renderer'; -export * from './autocomplete.renderer'; +export * from './enum.renderer'; diff --git a/packages/angular-material/src/library/index.ts b/packages/angular-material/src/library/index.ts index 64c286e7d..2a32c6c2d 100644 --- a/packages/angular-material/src/library/index.ts +++ b/packages/angular-material/src/library/index.ts @@ -54,9 +54,11 @@ import { ToggleControlRendererTester, } from './controls/toggle.renderer'; import { - AutocompleteControlRenderer, + EnumControlRenderer, enumControlTester, -} from './controls/autocomplete.renderer'; + OneOfEnumControlRenderer, + oneOfEnumControlTester, +} from './controls/enum.renderer'; import { ObjectControlRenderer, ObjectControlRendererTester, @@ -106,7 +108,8 @@ export const angularMaterialRenderers: { { tester: RangeControlRendererTester, renderer: RangeControlRenderer }, { tester: DateControlRendererTester, renderer: DateControlRenderer }, { tester: ToggleControlRendererTester, renderer: ToggleControlRenderer }, - { tester: enumControlTester, renderer: AutocompleteControlRenderer }, + { tester: enumControlTester, renderer: EnumControlRenderer }, + { tester: oneOfEnumControlTester, renderer: OneOfEnumControlRenderer }, { tester: ObjectControlRendererTester, renderer: ObjectControlRenderer }, // layouts { tester: verticalLayoutTester, renderer: VerticalLayoutRenderer }, diff --git a/packages/angular-material/src/library/module.ts b/packages/angular-material/src/library/module.ts index bee96ef8f..f286658e1 100644 --- a/packages/angular-material/src/library/module.ts +++ b/packages/angular-material/src/library/module.ts @@ -45,9 +45,12 @@ import { MatTabsModule } from '@angular/material/tabs'; import { MatToolbarModule } from '@angular/material/toolbar'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { JsonFormsModule } from '@jsonforms/angular'; -import { AutocompleteControlRenderer } from './controls/autocomplete.renderer'; import { BooleanControlRenderer } from './controls/boolean.renderer'; import { DateControlRenderer } from './controls/date.renderer'; +import { + EnumControlRenderer, + OneOfEnumControlRenderer, +} from './controls/enum.renderer'; import { NumberControlRenderer } from './controls/number.renderer'; import { RangeControlRenderer } from './controls/range.renderer'; import { TextAreaRenderer } from './controls/textarea.renderer'; @@ -104,7 +107,8 @@ import { LayoutChildrenRenderPropsPipe } from './layouts'; MasterListComponent, JsonFormsDetailComponent, ObjectControlRenderer, - AutocompleteControlRenderer, + EnumControlRenderer, + OneOfEnumControlRenderer, TableRenderer, ArrayLayoutRenderer, LayoutChildrenRenderPropsPipe, @@ -144,7 +148,8 @@ import { LayoutChildrenRenderPropsPipe } from './layouts'; MasterListComponent, JsonFormsDetailComponent, ObjectControlRenderer, - AutocompleteControlRenderer, + EnumControlRenderer, + OneOfEnumControlRenderer, TableRenderer, ArrayLayoutRenderer, LayoutChildrenRenderPropsPipe, diff --git a/packages/angular-material/src/library/other/master-detail/master.ts b/packages/angular-material/src/library/other/master-detail/master.ts index 7e8ad9e49..ec865c9ef 100644 --- a/packages/angular-material/src/library/other/master-detail/master.ts +++ b/packages/angular-material/src/library/other/master-detail/master.ts @@ -101,7 +101,7 @@ export const removeSchemaKeywords = (path: string) => { mat-icon-button class="button item-button hide" (click)="onDeleteClick(i)" - [ngClass]="{ show: highlightedIdx == i }" + [ngClass]="{ show: highlightedIdx === i }" *ngIf="isEnabled()" > delete diff --git a/packages/angular-material/test/array-layout.spec.ts b/packages/angular-material/test/array-layout.spec.ts index 47a94dd51..ac8aeef55 100644 --- a/packages/angular-material/test/array-layout.spec.ts +++ b/packages/angular-material/test/array-layout.spec.ts @@ -125,11 +125,11 @@ describe('Array layout', () => { const arrayLayoutElement: HTMLElement = fixture.nativeElement; const matBadgeElement = - arrayLayoutElement.querySelector('.mat-badge-content')!; + arrayLayoutElement.querySelector('.mat-badge-content'); const noDataElement = arrayLayoutElement.children[0].children[1]; - expect(matBadgeElement.textContent).toBe('1'); + expect(matBadgeElement?.textContent).toBe('1'); expect(noDataElement.textContent).toBe('No data'); }); }); @@ -149,9 +149,9 @@ describe('Array layout', () => { const arrayLayoutElement: HTMLElement = fixture.nativeElement; const matBadgeElement = - arrayLayoutElement.querySelector('.mat-badge-content')!; + arrayLayoutElement.querySelector('.mat-badge-content'); - expect(matBadgeElement.textContent).toBe('2'); + expect(matBadgeElement?.textContent).toBe('2'); }); }); @@ -170,9 +170,9 @@ describe('Array layout', () => { const arrayLayoutElement: HTMLElement = fixture.nativeElement; const matBadgeElement = - arrayLayoutElement.querySelector('.mat-badge-content')!; + arrayLayoutElement.querySelector('.mat-badge-content'); - expect(matBadgeElement.textContent).toBe('4'); + expect(matBadgeElement?.textContent).toBe('4'); }); }); }); diff --git a/packages/angular-material/test/autocomplete-control.spec.ts b/packages/angular-material/test/enum-control.spec.ts similarity index 93% rename from packages/angular-material/test/autocomplete-control.spec.ts rename to packages/angular-material/test/enum-control.spec.ts index fe685bf15..a99fc8837 100644 --- a/packages/angular-material/test/autocomplete-control.spec.ts +++ b/packages/angular-material/test/enum-control.spec.ts @@ -51,7 +51,7 @@ import { JsonFormsCore, EnumOption, } from '@jsonforms/core'; -import { AutocompleteControlRenderer } from '../src'; +import { EnumControlRenderer } from '../src'; import { JsonFormsAngularService } from '@jsonforms/angular'; import { ErrorObject } from 'ajv'; import { HarnessLoader } from '@angular/cdk/testing'; @@ -81,16 +81,16 @@ const imports = [ ReactiveFormsModule, ]; const providers = [JsonFormsAngularService]; -const componentUT: any = AutocompleteControlRenderer; +const componentUT: any = EnumControlRenderer; const errorTest: ErrorTestExpectation = { errorInstance: MatError, numberOfElements: 1, indexOfElement: 0, }; -describe('Autocomplete control Base Tests', () => { - let fixture: ComponentFixture; - let component: AutocompleteControlRenderer; +describe('Enum control Base Tests', () => { + let fixture: ComponentFixture; + let component: EnumControlRenderer; let inputElement: HTMLInputElement; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ @@ -215,9 +215,9 @@ describe('Autocomplete control Base Tests', () => { expect(inputElement.id).toBe('myId'); }); }); -describe('AutoComplete control Input Event Tests', () => { - let fixture: ComponentFixture; - let component: AutocompleteControlRenderer; +describe('Enum control Input Event Tests', () => { + let fixture: ComponentFixture; + let component: EnumControlRenderer; let loader: HarnessLoader; let inputElement: HTMLInputElement; beforeEach(waitForAsync(() => { @@ -269,7 +269,7 @@ describe('AutoComplete control Input Event Tests', () => { getJsonFormsService(component).updateCore( Actions.init(data, schema, uischema) ); - component.options = ['X', 'Y', 'Z']; + component.stringOptions = ['X', 'Y', 'Z']; component.ngOnInit(); fixture.detectChanges(); @@ -339,9 +339,9 @@ describe('AutoComplete control Input Event Tests', () => { expect(inputElement.value).toBe('Translated B'); })); }); -describe('AutoComplete control Error Tests', () => { - let fixture: ComponentFixture; - let component: AutocompleteControlRenderer; +describe('Enum control Error Tests', () => { + let fixture: ComponentFixture; + let component: EnumControlRenderer; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [componentUT, ...imports], @@ -383,9 +383,9 @@ describe('AutoComplete control Error Tests', () => { }); }); -describe('AutoComplete control updateFilter function', () => { - let fixture: ComponentFixture; - let component: AutocompleteControlRenderer; +describe('Enum control updateFilter function', () => { + let fixture: ComponentFixture; + let component: EnumControlRenderer; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ @@ -401,7 +401,7 @@ describe('AutoComplete control updateFilter function', () => { it('should not filter options on ENTER key press', () => { component.shouldFilter = false; - component.options = ['X', 'Y', 'Z']; + component.stringOptions = ['X', 'Y', 'Z']; setupMockStore(fixture, { uischema, schema, data }); getJsonFormsService(component).updateCore( Actions.init(data, schema, uischema) @@ -415,7 +415,7 @@ describe('AutoComplete control updateFilter function', () => { it('should filter options when a key other than ENTER is pressed', () => { component.shouldFilter = false; - component.options = ['X', 'Y', 'Z']; + component.stringOptions = ['X', 'Y', 'Z']; setupMockStore(fixture, { uischema, schema, data }); getJsonFormsService(component).updateCore( Actions.init(data, schema, uischema) diff --git a/packages/angular-material/test/one-of-enum-control.spec.ts b/packages/angular-material/test/one-of-enum-control.spec.ts new file mode 100644 index 000000000..1367265a0 --- /dev/null +++ b/packages/angular-material/test/one-of-enum-control.spec.ts @@ -0,0 +1,506 @@ +/* + 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 { + MatAutocompleteModule, + MatAutocompleteSelectedEvent, +} from '@angular/material/autocomplete'; +import { MatError, MatFormFieldModule } from '@angular/material/form-field'; +import { MatInputModule } from '@angular/material/input'; +import { DebugElement } from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import { ReactiveFormsModule } from '@angular/forms'; +import { By } from '@angular/platform-browser'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { + ErrorTestExpectation, + getJsonFormsService, + setupMockStore, +} from './common'; +import { + Actions, + ControlElement, + EnumOption, + JsonFormsCore, + JsonSchema, +} from '@jsonforms/core'; +import { OneOfEnumControlRenderer } from '../src'; +import { JsonFormsAngularService } from '@jsonforms/angular'; +import { ErrorObject } from 'ajv'; +import { HarnessLoader } from '@angular/cdk/testing'; +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { MatAutocompleteHarness } from '@angular/material/autocomplete/testing'; + +const data = { + oneOfEnum: 'foo', +}; +const schema: JsonSchema = { + type: 'object', + properties: { + oneOfEnum: { + type: 'string', + oneOf: [ + { + const: 'foo', + title: 'Foo', + }, + { + const: 'bar', + title: 'Bar', + }, + { + const: 'foobar', + title: 'FooBar', + }, + ], + }, + }, +}; +const uischema: ControlElement = { + type: 'Control', + scope: '#/properties/oneOfEnum', +}; + +const imports = [ + MatAutocompleteModule, + MatInputModule, + MatFormFieldModule, + NoopAnimationsModule, + ReactiveFormsModule, +]; +const providers = [JsonFormsAngularService]; +const componentUT: any = OneOfEnumControlRenderer; +const errorTest: ErrorTestExpectation = { + errorInstance: MatError, + numberOfElements: 1, + indexOfElement: 0, +}; + +describe('OneOfEnum control Base Tests', () => { + let fixture: ComponentFixture; + let component: OneOfEnumControlRenderer; + let inputElement: HTMLInputElement; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [componentUT, ...imports], + providers: providers, + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(componentUT); + component = fixture.componentInstance; + + inputElement = fixture.debugElement.query(By.css('input')).nativeElement; + }); + + it('should render', fakeAsync(() => { + setupMockStore(fixture, { uischema, schema, data }); + getJsonFormsService(component).updateCore( + Actions.init(data, schema, uischema) + ); + component.ngOnInit(); + fixture.detectChanges(); + tick(); + expect(component.data).toEqual('foo'); + expect(inputElement.value).toBe('Foo'); + expect(inputElement.disabled).toBe(false); + })); + + it('should support updating the state', fakeAsync(() => { + setupMockStore(fixture, { uischema, schema, data }); + getJsonFormsService(component).updateCore( + Actions.init(data, schema, uischema) + ); + component.ngOnInit(); + fixture.detectChanges(); + tick(); + getJsonFormsService(component).updateCore( + Actions.update('oneOfEnum', () => 'bar') + ); + tick(); + fixture.detectChanges(); + expect(component.data).toEqual('bar'); + expect(inputElement.value).toBe('Bar'); + })); + + it('should update with undefined value', () => { + setupMockStore(fixture, { uischema, schema, data }); + getJsonFormsService(component).updateCore( + Actions.init(data, schema, uischema) + ); + component.ngOnInit(); + fixture.detectChanges(); + + getJsonFormsService(component).updateCore( + Actions.update('oneOfEnum', () => undefined) + ); + fixture.detectChanges(); + expect(component.data).toBe(undefined); + expect(inputElement.value).toBe(''); + }); + + it('should update with null value', () => { + setupMockStore(fixture, { uischema, schema, data }); + getJsonFormsService(component).updateCore( + Actions.init(data, schema, uischema) + ); + fixture.detectChanges(); + component.ngOnInit(); + + getJsonFormsService(component).updateCore( + Actions.update('oneOfEnum', () => null) + ); + fixture.detectChanges(); + expect(component.data).toBe(null); + expect(inputElement.value).toBe(''); + }); + + it('should not update with wrong ref', fakeAsync(() => { + setupMockStore(fixture, { uischema, schema, data }); + getJsonFormsService(component).updateCore( + Actions.init(data, schema, uischema) + ); + component.ngOnInit(); + fixture.detectChanges(); + tick(); + getJsonFormsService(component).updateCore( + Actions.update('oneOfEnum', () => 'foo') + ); + getJsonFormsService(component).updateCore( + Actions.update('plainEnum', () => 'bar') + ); + fixture.detectChanges(); + tick(); + expect(component.data).toEqual('foo'); + expect(inputElement.value).toBe('Foo'); + })); + + // store needed as we evaluate the calculated enabled value to disable/enable the control + it('can be disabled', () => { + setupMockStore(fixture, { uischema, schema, data }); + getJsonFormsService(component).updateCore( + Actions.init(data, schema, uischema) + ); + component.disabled = true; + component.ngOnInit(); + fixture.detectChanges(); + expect(inputElement.disabled).toBe(true); + }); + + it('can be hidden', () => { + setupMockStore(fixture, { uischema, schema, data }); + getJsonFormsService(component).updateCore( + Actions.init(data, schema, uischema) + ); + component.visible = false; + component.ngOnInit(); + fixture.detectChanges(); + const hasDisplayNone = + 'none' === fixture.nativeElement.children[0].style.display; + const hasHidden = fixture.nativeElement.children[0].hidden; + expect(hasDisplayNone || hasHidden).toBeTruthy(); + }); + + it('id should be present in output', () => { + setupMockStore(fixture, { uischema, schema, data }); + component.id = 'myId'; + getJsonFormsService(component).updateCore( + Actions.init(data, schema, uischema) + ); + + fixture.detectChanges(); + component.ngOnInit(); + expect(inputElement.id).toBe('myId'); + }); +}); + +describe('OneOfEnum control Input Event Tests', () => { + let fixture: ComponentFixture; + let component: OneOfEnumControlRenderer; + let loader: HarnessLoader; + let inputElement: HTMLInputElement; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [componentUT, ...imports], + providers: [...providers], + }).compileComponents(); + })); + + beforeEach(waitForAsync(() => { + fixture = TestBed.createComponent(componentUT); + component = fixture.componentInstance; + loader = TestbedHarnessEnvironment.loader(fixture); + + inputElement = fixture.debugElement.query(By.css('input')).nativeElement; + })); + + it('should update via input event', fakeAsync(async () => { + setupMockStore(fixture, { uischema, schema, data }); + getJsonFormsService(component).updateCore( + Actions.init(data, schema, uischema) + ); + + component.ngOnInit(); + fixture.detectChanges(); + + const spy = spyOn(component, 'onSelect'); + + await (await loader.getHarness(MatAutocompleteHarness)).focus(); + fixture.detectChanges(); + + await ( + await loader.getHarness(MatAutocompleteHarness) + ).selectOption({ text: 'Bar' }); + tick(); + fixture.detectChanges(); + + expect(spy).toHaveBeenCalled(); + const event = spy.calls.mostRecent() + .args[0] as MatAutocompleteSelectedEvent; + + expect(event.option.value).toEqual({ + label: 'Bar', + value: 'bar', + } satisfies EnumOption); + expect(inputElement.value).toBe('Bar'); + })); + + it('options should prefer own props', fakeAsync(async () => { + setupMockStore(fixture, { uischema, schema, data }); + getJsonFormsService(component).updateCore( + Actions.init(data, schema, uischema) + ); + component.options = [ + { + label: 'X', + value: 'x', + }, + { + label: 'Y', + value: 'y', + }, + { + label: 'Z', + value: 'z', + }, + ]; + + component.ngOnInit(); + fixture.detectChanges(); + const spy = spyOn(component, 'onSelect'); + + await (await loader.getHarness(MatAutocompleteHarness)).focus(); + fixture.detectChanges(); + + await ( + await loader.getHarness(MatAutocompleteHarness) + ).selectOption({ text: 'Y' }); + fixture.detectChanges(); + tick(); + + const event = spy.calls.mostRecent() + .args[0] as MatAutocompleteSelectedEvent; + expect(event.option.value).toEqual({ + label: 'Y', + value: 'y', + } satisfies EnumOption); + expect(inputElement.value).toBe('Y'); + })); + + it('should render translated enum correctly', fakeAsync(async () => { + setupMockStore(fixture, { uischema, schema, data }); + const state: JsonFormsCore = { + data, + schema, + uischema, + }; + getJsonFormsService(component).init({ + core: state, + i18n: { + translate: (key, defaultMessage) => { + const translations: { [key: string]: string } = { + 'oneOfEnum.Foo': 'Translated Foo', + 'oneOfEnum.Bar': 'Translated Bar', + 'oneOfEnum.FooBar': 'Translated FooBar', + }; + return translations[key] ?? defaultMessage; + }, + }, + }); + getJsonFormsService(component).updateCore( + Actions.init(data, schema, uischema) + ); + component.ngOnInit(); + fixture.detectChanges(); + const spy = spyOn(component, 'onSelect'); + + await (await loader.getHarness(MatAutocompleteHarness)).focus(); + fixture.detectChanges(); + + await ( + await loader.getHarness(MatAutocompleteHarness) + ).selectOption({ + text: 'Translated Bar', + }); + fixture.detectChanges(); + tick(); + + const event = spy.calls.mostRecent() + .args[0] as MatAutocompleteSelectedEvent; + expect(event.option.value).toEqual({ + label: 'Translated Bar', + value: 'bar', + } satisfies EnumOption); + expect(inputElement.value).toBe('Translated Bar'); + })); +}); + +describe('OneOfEnum control Error Tests', () => { + let fixture: ComponentFixture; + let component: OneOfEnumControlRenderer; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [componentUT, ...imports], + providers: providers, + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(componentUT); + component = fixture.componentInstance; + }); + + it('should display errors', () => { + const errors: ErrorObject[] = [ + { + instancePath: '/oneOfEnum', + message: 'Hi, this is me, test error!', + params: {}, + keyword: '', + schemaPath: '', + }, + ]; + setupMockStore(fixture, { + uischema, + schema, + data, + }); + const formsService = getJsonFormsService(component); + formsService.updateCore(Actions.updateErrors(errors)); + formsService.refresh(); + + component.ngOnInit(); + fixture.detectChanges(); + const debugErrors: DebugElement[] = fixture.debugElement.queryAll( + By.directive(errorTest.errorInstance) + ); + expect(debugErrors.length).toBe(errorTest.numberOfElements); + expect( + debugErrors[errorTest.indexOfElement].nativeElement.textContent + ).toBe('Hi, this is me, test error!'); + }); +}); + +describe('OneOfEnum control updateFilter function', () => { + let fixture: ComponentFixture; + let component: OneOfEnumControlRenderer; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [componentUT, ...imports], + providers: providers, + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(componentUT); + component = fixture.componentInstance; + }); + + it('should not filter options on ENTER key press', () => { + component.shouldFilter = false; + component.options = [ + { + label: 'X', + value: 'x', + }, + { + label: 'Y', + value: 'y', + }, + { + label: 'Z', + value: 'z', + }, + ]; + setupMockStore(fixture, { uischema, schema, data }); + getJsonFormsService(component).updateCore( + Actions.init(data, schema, uischema) + ); + component.ngOnInit(); + fixture.detectChanges(); + component.updateFilter({ keyCode: 13 }); + fixture.detectChanges(); + expect(component.shouldFilter).toBe(false); + }); + + it('should filter options when a key other than ENTER is pressed', () => { + component.shouldFilter = false; + component.options = [ + { + label: 'X', + value: 'x', + }, + { + label: 'Y', + value: 'y', + }, + { + label: 'Z', + value: 'z', + }, + ]; + setupMockStore(fixture, { uischema, schema, data }); + getJsonFormsService(component).updateCore( + Actions.init(data, schema, uischema) + ); + component.ngOnInit(); + fixture.detectChanges(); + + component.updateFilter({ keyCode: 65 }); + fixture.detectChanges(); + + expect(component.shouldFilter).toBe(true); + }); +});