Skip to content

Commit

Permalink
[AAE-7765] Improved display mandatory form fields (#7531)
Browse files Browse the repository at this point in the history
* [MNT-22765] Improved display mandatory form fields

* [MNT-22765] added unit tests

* [MNT-22765] fixed test with error icon on rest fail

* Trigger travis

* [MNT-22765] removed underscore from var name

* [AAE-7765] removed underscore from unit test

* [AAE-7765] fixed css lint

* [AAE-7765] fixed e2e error message css class

* [AAE-7765] fixed storybook e2e
  • Loading branch information
tomgny committed Mar 7, 2022
1 parent e877cd8 commit 3dc9f7c
Show file tree
Hide file tree
Showing 67 changed files with 915 additions and 219 deletions.
Expand Up @@ -42,6 +42,7 @@ Searches Groups.
| mode | [`ComponentSelectionMode`](../../../lib/process-services-cloud/src/lib/types.ts) | "single" | Group selection mode (single/multiple). |
| preSelectGroups | [`IdentityGroupModel`](../../../lib/core/models/identity-group.model.ts)`[]` | \[] | Array of groups to be pre-selected. This pre-selects all groups in multi selection mode and only the first group of the array in single selection mode. |
| readOnly | `boolean` | false | Show the info in readonly mode |
| required | `boolean` | false | Mark this field as required |
| roles | `string[]` | \[] | Role names of the groups to be listed. |
| searchGroupsControl | `FormControl` | | FormControl to search the group |
| title | `string` | | Title of the field |
Expand Down
Expand Up @@ -30,6 +30,7 @@ Allows one or more users to be selected (with auto-suggestion) based on the inpu
| mode | [`ComponentSelectionMode`](../../../lib/process-services-cloud/src/lib/types.ts) | "single" | User selection mode (single/multiple). |
| preSelectUsers | [`IdentityUserModel`](../../../lib/core/models/identity-user.model.ts)`[]` | \[] | Array of users to be pre-selected. All users in the array are pre-selected in multi selection mode, but only the first user is pre-selected in single selection mode. Mandatory properties are: id, email, username |
| readOnly | `boolean` | false | Show the info in readonly mode |
| required | `boolean` | false | Mark this field as required |
| roles | `string[]` | | Role names of the users to be listed. |
| searchUserCtrl | `FormControl` | | FormControl to search the user |
| title | `string` | | Placeholder translation key |
Expand Down
Expand Up @@ -40,11 +40,12 @@ test.describe('Groups component stories tests', () => {
});

test('Invalid Preselected Groups', async ({ processServicesCloud, groupComponent }) => {
const expectedWarningMessage = 'warning No group found with the name invalid groups';
const expectedWarningMessage = 'No group found with the name invalid groups';
const expectedWarningIcon = 'error_outline';

await processServicesCloud.navigateTo({ componentName: 'group', story: 'invalid-preselected-groups' });

await expect(groupComponent.error.content).toContainText(expectedWarningMessage);
await expect(groupComponent.error.content).toContainText(expectedWarningIcon + expectedWarningMessage);
});

});
Expand Up @@ -39,11 +39,12 @@ test.describe('People component stories tests', () => {
});

test('Invalid Preselected Users', async ({ processServicesCloud, peopleComponent }) => {
const expectedWarningMessage = 'warning No user found with the username invalid user';
const expectedWarningMessage = 'No user found with the username invalid user';
const expectedWarningIcon = 'error_outline';

await processServicesCloud.navigateTo({ componentName: 'people', story: 'invalid-preselected-users' });

await expect(peopleComponent.error.content).toContainText(expectedWarningMessage);
await expect(peopleComponent.error.content).toContainText(expectedWarningIcon + expectedWarningMessage);
});

test('Excluded Users', async ({ processServicesCloud, peopleComponent }) => {
Expand Down
25 changes: 14 additions & 11 deletions lib/core/form/components/form-renderer.component.spec.ts
Expand Up @@ -45,43 +45,39 @@ import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';

const typeIntoInput = (targetInput: HTMLInputElement, message: string) => {
expect(targetInput).not.toBeNull('Expected input to set to be valid and not null');
expect(targetInput).toBeTruthy('Expected input to set to be valid and not null');
targetInput.value = message;
targetInput.dispatchEvent(new Event('input'));
};

const typeIntoDate = (targetInput: DebugElement, date: { srcElement: { value: string } }) => {
expect(targetInput).not.toBeNull('Expected input to set to be valid and not null');
expect(targetInput).toBeTruthy('Expected input to set to be valid and not null');
targetInput.triggerEventHandler('change', date);
};

const expectElementToBeHidden = (targetElement: HTMLElement): void => {
expect(targetElement).not.toBeNull();
expect(targetElement).toBeDefined();
expect(targetElement).toBeTruthy();
expect(targetElement.hidden).toBe(true, `${targetElement.id} should be hidden but it is not`);
};

const expectElementToBeVisible = (targetElement: HTMLElement): void => {
expect(targetElement).not.toBeNull();
expect(targetElement).toBeDefined();
expect(targetElement).toBeTruthy();
expect(targetElement.hidden).toBe(false, `${targetElement.id} should be visibile but it is not`);
};

const expectInputElementValueIs = (targetElement: HTMLInputElement, value: string): void => {
expect(targetElement).not.toBeNull();
expect(targetElement).toBeDefined();
expect(targetElement).toBeTruthy();
expect(targetElement.value).toBe(value, `invalid value for ${targetElement.name}`);
};

const expectElementToBeInvalid = (fieldId: string, fixture: ComponentFixture<FormRendererComponent>): void => {
const invalidElementContainer = fixture.nativeElement.querySelector(`#field-${fieldId}-container .adf-invalid`);
expect(invalidElementContainer).not.toBeNull();
expect(invalidElementContainer).toBeDefined();
expect(invalidElementContainer).toBeTruthy();
};

const expectElementToBeValid = (fieldId: string, fixture: ComponentFixture<FormRendererComponent>): void => {
const invalidElementContainer = fixture.nativeElement.querySelector(`#field-${fieldId}-container .adf-invalid`);
expect(invalidElementContainer).toBeNull();
expect(invalidElementContainer).toBeFalsy();
};

describe('Form Renderer Component', () => {
Expand Down Expand Up @@ -407,6 +403,12 @@ describe('Form Renderer Component', () => {

const numberInputRequired: HTMLInputElement = fixture.nativeElement.querySelector('#Number0x8cbv');
expectElementToBeVisible(numberInputRequired);
expectElementToBeValid('Number0x8cbv', fixture);

numberInputRequired.dispatchEvent(new Event('blur'));
fixture.detectChanges();
await fixture.whenStable();

expectElementToBeInvalid('Number0x8cbv', fixture);

typeIntoInput(numberInputRequired, '5');
Expand Down Expand Up @@ -444,6 +446,7 @@ describe('Form Renderer Component', () => {
expectElementToBeVisible(numberInputElement);
expectElementToBeValid('Number0him2z', fixture);

numberInputElement.dispatchEvent(new Event('blur'));
typeIntoInput(numberInputElement, '9');
fixture.detectChanges();
await fixture.whenStable();
Expand Down
9 changes: 5 additions & 4 deletions lib/core/form/components/widgets/amount/amount.widget.html
@@ -1,8 +1,8 @@
<div class="adf-amount-widget__container adf-amount-widget {{field.className}}"
[class.adf-invalid]="!field.isValid"
[class.adf-invalid]="!field.isValid && isTouched()"
[class.adf-readonly]="field.readOnly">
<label class="adf-label"
[attr.for]="field.id">{{field.name | translate }}<span *ngIf="isRequired()">*</span></label>
[attr.for]="field.id">{{field.name | translate }}<span class="adf-asterisk" *ngIf="isRequired()">*</span></label>
<mat-form-field class="adf-amount-widget__input" [hideRequiredMarker]="true">
<span matPrefix class="adf-amount-widget__prefix-spacing">{{ currency }} &nbsp;</span>
<input matInput
Expand All @@ -17,9 +17,10 @@
[value]="field.value"
[(ngModel)]="field.value"
(ngModelChange)="onFieldChanged(field)"
[disabled]="field.readOnly">
[disabled]="field.readOnly"
(blur)="markAsTouched()">
</mat-form-field>
<error-widget [error]="field.validationSummary"></error-widget>
<error-widget *ngIf="isInvalidFieldRequired()"
<error-widget *ngIf="isInvalidFieldRequired() && isTouched()"
required="{{ 'FORM.FIELD.REQUIRED' | translate }}"></error-widget>
</div>
38 changes: 36 additions & 2 deletions lib/core/form/components/widgets/amount/amount.widget.spec.ts
Expand Up @@ -20,14 +20,16 @@ import { FormFieldModel } from './../core/form-field.model';
import { AmountWidgetComponent, ADF_AMOUNT_SETTINGS } from './amount.widget';
import { setupTestBed } from '../../../../testing/setup-test-bed';
import { FormBaseModule } from '../../../form-base.module';
import { FormModel } from '../core';
import { FormFieldTypes } from '../core/form-field-types';
import { CoreTestingModule } from '../../../../testing/core.testing.module';
import { TranslateModule } from '@ngx-translate/core';
import { FormModel } from '../core/form.model';

describe('AmountWidgetComponent', () => {

let widget: AmountWidgetComponent;
let fixture: ComponentFixture<AmountWidgetComponent>;
let element: HTMLElement;

setupTestBed({
imports: [
Expand All @@ -39,8 +41,8 @@ describe('AmountWidgetComponent', () => {

beforeEach(() => {
fixture = TestBed.createComponent(AmountWidgetComponent);

widget = fixture.componentInstance;
element = fixture.nativeElement;
});

it('should setup currency from field', () => {
Expand Down Expand Up @@ -78,6 +80,38 @@ describe('AmountWidgetComponent', () => {
widget.ngOnInit();
expect(widget.placeholder).toBe('1234');
});

describe('when is required', () => {

beforeEach(() => {
widget.field = new FormFieldModel( new FormModel({ taskId: '<id>' }), {
type: FormFieldTypes.AMOUNT,
required: true
});
});

it('should be marked as invalid after interaction', async () => {
const amount = fixture.nativeElement.querySelector('input');
expect(element.querySelector('.adf-invalid')).toBeFalsy();

amount.dispatchEvent(new Event('blur'));

fixture.detectChanges();
await fixture.whenStable();

expect(fixture.nativeElement.querySelector('.adf-invalid')).toBeTruthy();
});

it('should be able to display label with asterisk', async () => {
fixture.detectChanges();
await fixture.whenStable();

const asterisk: HTMLElement = element.querySelector('.adf-asterisk');

expect(asterisk).toBeTruthy();
expect(asterisk.textContent).toEqual('*');
});
});
});

describe('AmountWidgetComponent - rendering', () => {
Expand Down
@@ -1,16 +1,19 @@
<div [ngClass]="field.className"
[class.adf-invalid]="!field.isValid">
[class.adf-invalid]="!field.isValid && isTouched()">
<mat-checkbox
[id]="field.id"
color="primary"
[required]="field.required"
[required]="isRequired()"
[disabled]="field.readOnly || readOnly"
[(ngModel)]="field.value"
(ngModelChange)="onFieldChanged(field)"
[matTooltip]="field.tooltip"
(click)="markAsTouched()"
matTooltipPosition="right"
matTooltipShowDelay="1000">
{{field.name | translate }}
<span *ngIf="field.required" >*</span>
<span class="adf-asterisk" *ngIf="isRequired()" >*</span>
</mat-checkbox>
<error-widget [error]="field.validationSummary"></error-widget>
<error-widget *ngIf="isInvalidFieldRequired() && isTouched()" required="{{ 'FORM.FIELD.REQUIRED' | translate }}"></error-widget>
</div>
24 changes: 20 additions & 4 deletions lib/core/form/components/widgets/checkbox/checkbox.widget.spec.ts
Expand Up @@ -21,9 +21,9 @@ import { FormFieldModel } from '../core/form-field.model';
import { FormModel } from '../core/form.model';
import { CheckboxWidgetComponent } from './checkbox.widget';
import { setupTestBed } from '../../../../testing/setup-test-bed';
import { FormBaseModule } from 'core/form/form-base.module';
import { FormBaseModule } from '../../../form-base.module';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { TranslateLoaderService } from 'core/services';
import { TranslateLoaderService } from '../../../../services/translate-loader.service';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { CoreTestingModule } from '../../../../testing';
import { MatTooltipModule } from '@angular/material/tooltip';
Expand Down Expand Up @@ -69,11 +69,27 @@ describe('CheckboxWidgetComponent', () => {
});
});

it('should be marked as invalid when required', async () => {
it('should be marked as invalid when required after interaction', async () => {
const checkbox = element.querySelector('mat-checkbox');
expect(element.querySelector('.adf-invalid')).toBeFalsy();

checkbox.dispatchEvent(new Event('click'));
checkbox.dispatchEvent(new Event('click'));

fixture.detectChanges();
await fixture.whenStable();

expect(element.querySelector('.adf-invalid')).toBeTruthy();
});

it('should be able to display label with asterisk', async () => {
fixture.detectChanges();
await fixture.whenStable();

expect(element.querySelector('.adf-invalid')).not.toBeNull();
const asterisk: HTMLElement = element.querySelector('.adf-asterisk');

expect(asterisk).toBeTruthy();
expect(asterisk.textContent).toEqual('*');
});

it('should be checked if boolean true is passed', fakeAsync(() => {
Expand Down
@@ -1,6 +1,6 @@
<div class="{{field.className}}" id="data-time-widget" [class.adf-invalid]="!field.isValid">
<div class="{{field.className}}" id="data-time-widget" [class.adf-invalid]="!field.isValid && isTouched()">
<mat-form-field class="adf-date-time-widget" [hideRequiredMarker]="true">
<label class="adf-label" [attr.for]="field.id">{{field.name | translate }} ({{field.dateDisplayFormat}})<span *ngIf="isRequired()">*</span></label>
<label class="adf-label" [attr.for]="field.id">{{field.name | translate }} ({{field.dateDisplayFormat}})<span class="adf-asterisk" *ngIf="isRequired()">*</span></label>
<input matInput
[id]="field.id"
[value]="field.value"
Expand All @@ -9,13 +9,14 @@
(change)="onDateChanged($any($event).srcElement.value)"
[placeholder]="field.placeholder"
[matTooltip]="field.tooltip"
(blur)="markAsTouched()"
matTooltipPosition="above"
matTooltipShowDelay="1000"
(focus)="datetimePicker.open()">
<mat-datetimepicker-toggle matSuffix [for]="datetimePicker" [disabled]="field.readOnly"></mat-datetimepicker-toggle>
</mat-form-field>
<error-widget [error]="field.validationSummary"></error-widget>
<error-widget *ngIf="isInvalidFieldRequired()" required="{{ 'FORM.FIELD.REQUIRED' | translate }}"></error-widget>
<error-widget *ngIf="isInvalidFieldRequired() && isTouched()" required="{{ 'FORM.FIELD.REQUIRED' | translate }}"></error-widget>
<mat-datetimepicker #datetimePicker type="datetime" [touchUi]="true" [timeInterval]="5" [disabled]="field.readOnly"></mat-datetimepicker>
<input
type="hidden"
Expand Down
Expand Up @@ -24,6 +24,7 @@ import { setupTestBed } from '../../../../testing/setup-test-bed';
import { CoreTestingModule } from '../../../../testing/core.testing.module';
import { TranslateModule } from '@ngx-translate/core';
import { MatTooltipModule } from '@angular/material/tooltip';
import { FormFieldTypes } from '../core/form-field-types';

describe('DateTimeWidgetComponent', () => {

Expand Down Expand Up @@ -106,6 +107,38 @@ describe('DateTimeWidgetComponent', () => {
expect(widget.onFieldChanged).toHaveBeenCalledWith(field);
});

describe('when is required', () => {

beforeEach(() => {
widget.field = new FormFieldModel( new FormModel({ taskId: '<id>' }), {
type: FormFieldTypes.DATETIME,
required: true
});
});

it('should be marked as invalid after interaction', async () => {
const dateTimeInput = fixture.nativeElement.querySelector('input');
expect(fixture.nativeElement.querySelector('.adf-invalid')).toBeFalsy();

dateTimeInput.dispatchEvent(new Event('blur'));

fixture.detectChanges();
await fixture.whenStable();

expect(fixture.nativeElement.querySelector('.adf-invalid')).toBeTruthy();
});

it('should be able to display label with asterisk', async () => {
fixture.detectChanges();
await fixture.whenStable();

const asterisk: HTMLElement = element.querySelector('.adf-asterisk');

expect(asterisk).toBeTruthy();
expect(asterisk.textContent).toEqual('*');
});
});

describe('template check', () => {

it('should show visible date widget', async () => {
Expand Down
10 changes: 6 additions & 4 deletions lib/core/form/components/widgets/date/date.widget.html
@@ -1,17 +1,19 @@
<div class="{{field.className}}" id="data-widget" [class.adf-invalid]="!field.isValid">
<div class="{{field.className}}" id="data-widget" [class.adf-invalid]="!field.isValid && isTouched()">
<mat-form-field class="adf-date-widget" [hideRequiredMarker]="true">
<label class="adf-label" [attr.for]="field.id">{{field.name | translate }} ({{field.dateDisplayFormat}})<span *ngIf="isRequired()">*</span></label>
<label class="adf-label" [attr.for]="field.id">{{field.name | translate }} ({{field.dateDisplayFormat}})<span class="adf-asterisk"
*ngIf="isRequired()">*</span></label>
<input matInput
[id]="field.id"
[value]="field.value"
[required]="isRequired()"
[disabled]="field.readOnly"
(change)="onDateChanged($any($event).srcElement.value)"
[placeholder]="field.placeholder">
[placeholder]="field.placeholder"
(blur)="markAsTouched()">
<mat-datepicker-toggle matSuffix [for]="datePicker" [disabled]="field.readOnly" ></mat-datepicker-toggle>
</mat-form-field>
<error-widget [error]="field.validationSummary"></error-widget>
<error-widget *ngIf="isInvalidFieldRequired()" required="{{ 'FORM.FIELD.REQUIRED' | translate }}"></error-widget>
<error-widget *ngIf="isInvalidFieldRequired() && isTouched()" required="{{ 'FORM.FIELD.REQUIRED' | translate }}"></error-widget>
<mat-datepicker #datePicker [touchUi]="true" [startAt]="field.value | adfMomentDate: field.dateDisplayFormat" [disabled]="field.readOnly"></mat-datepicker>
<input
type="hidden"
Expand Down

0 comments on commit 3dc9f7c

Please sign in to comment.