Skip to content

Commit

Permalink
feat: support of date attribute type (#18730)
Browse files Browse the repository at this point in the history
Closes CXSPA-6790
  • Loading branch information
ChristophHi authored Aug 6, 2024
1 parent e24eee4 commit 3ca1c6a
Show file tree
Hide file tree
Showing 17 changed files with 511 additions and 103 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -155,13 +155,15 @@
"nameOfAttribute": "Name of Attribute",
"valueOfAttribute": "Value of attribute {{ attribute }}",
"forAttribute": "{{ value }} for attribute {{ attribute }}",
"valueOfAttributeFull": "Value {{ value }} of attribute {{ attribute }}",
"valueOfAttributeFull": "Value {{ value }} of attribute {{ attribute }}.",
"valueOfDateAttributeFull": "Value {{ value }} of date attribute {{ attribute }}. Press space key to enter date picker.",
"valueOfAttributeFullWithPrice": "Value {{ value }} of attribute {{ attribute }}, Surcharge {{ price }}",
"selectedValueOfAttributeFull": "Selected value {{ value }} of attribute {{ attribute }}",
"selectedValueOfAttributeFullWithPrice": "Selected value {{ value }} of attribute {{ attribute }}, Surcharge {{ price }}",
"readOnlyValueOfAttributeFullWithPrice": "Read-only value {{ value }} of attribute {{ attribute }}, Surcharge {{ price }}",
"readOnlyValueOfAttributeFull": "Read-only value {{ value }} of attribute {{ attribute }}",
"valueOfAttributeBlank": "Value of attribute {{ attribute }} is blank",
"valueOfAttributeBlank": "Value of attribute {{ attribute }} is blank.",
"valueOfDateAttributeBlank": "Value of date attribute {{ attribute }} is blank. Press space key to enter date picker",
"value": "Value {{ value }}",
"attribute": "Attribute {{ attribute }}",
"requiredAttribute": "Attribute {{param}} is required",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
*/

export * from './composition/index';
export * from './price-change/index';
export * from './footer/index';
export * from './header/index';
export * from './price-change/index';
export * from './product-card/index';
export * from './quantity/index';
export * from './types/base/index';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,13 @@ describe('ConfiguratorAttributeSingleSelectionBaseComponent', () => {
component.attribute.validationType = Configurator.ValidationType.NUMERIC;
expect(component.isAdditionalValueNumeric).toBe(true);
});

it('should return false for UI type Radio button additional input and validation type sap date', () => {
component.attribute.uiType =
Configurator.UiType.DROPDOWN_ADDITIONAL_INPUT;
component.attribute.validationType = Configurator.ValidationType.SAP_DATE;
expect(component.isAdditionalValueNumeric).toBe(false);
});
});

describe('IsAdditionalValueAlphaNumeric', () => {
Expand All @@ -467,6 +474,13 @@ describe('ConfiguratorAttributeSingleSelectionBaseComponent', () => {
component.attribute.validationType = Configurator.ValidationType.NONE;
expect(component.isAdditionalValueAlphaNumeric).toBe(true);
});

it('should return true for UI type Radio button additional input and validation type sap date', () => {
component.attribute.uiType =
Configurator.UiType.DROPDOWN_ADDITIONAL_INPUT;
component.attribute.validationType = Configurator.ValidationType.SAP_DATE;
expect(component.isAdditionalValueAlphaNumeric).toBe(true);
});
});
describe('getAriaLabel', () => {
it('should return aria label for additional value', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ export abstract class ConfiguratorAttributeSingleSelectionBaseComponent extends
get isAdditionalValueAlphaNumeric(): boolean {
return (
this.isWithAdditionalValues(this.attribute) &&
this.attribute.validationType === Configurator.ValidationType.NONE
this.attribute.validationType !== Configurator.ValidationType.NUMERIC
);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,28 @@
<div id="{{ createAttributeIdForConfigurator(attribute) }}" class="form-group">
<label
*ngIf="inputType === 'date'"
class="cx-visually-hidden"
id="{{ createAttributeUiKey('labelForDate', attribute.name) }}"
[attr.aria-label]="
isUserInputEmpty
? ('configurator.a11y.valueOfDateAttributeBlank'
| cxTranslate
: {
attribute: attribute.label
})
: ('configurator.a11y.valueOfDateAttributeFull'
| cxTranslate
: {
value: attribute.userInput,
attribute: attribute.label
})
"
></label>
<input
[formControl]="attributeInputForm"
class="form-control"
type="{{ inputType }}"
(keydown)="activateDebounceDate()"
[ngClass]="{
'cx-required-error-msg ': (showRequiredErrorMessage$ | async)
}"
Expand All @@ -21,7 +42,11 @@
attribute: attribute.label
})
"
[attr.aria-describedby]="createAttributeUiKey('label', attribute.name)"
[attr.aria-describedby]="
inputType === 'date'
? createAttributeUiKey('labelForDate', attribute.name)
: createAttributeUiKey('label', attribute.name)
"
[cxFocus]="{
key: createAttributeIdForConfigurator(attribute)
}"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ import {
} from '@angular/core/testing';
import { ReactiveFormsModule } from '@angular/forms';
import { By } from '@angular/platform-browser';
import { FeaturesConfig, I18nTestingModule } from '@spartacus/core';
import { I18nTestingModule } from '@spartacus/core';
import { CommonConfigurator } from '@spartacus/product-configurator/common';
import { ConfiguratorStorefrontUtilsService } from '@spartacus/product-configurator/rulebased';
import { cold } from 'jasmine-marbles';
import { Observable, of } from 'rxjs';
import { CommonConfiguratorTestUtilsService } from '../../../../../common/testing/common-configurator-test-utils.service';
import { ConfiguratorCommonsService } from '../../../../core/facade/configurator-commons.service';
Expand All @@ -31,18 +32,19 @@ class MockConfiguratorCommonsService {
updateConfiguration(): void {}
}

const isCartEntryOrGroupVisited = true;
let isCartEntryOrGroupVisited = of(true);
class MockConfigUtilsService {
isCartEntryOrGroupVisited(): Observable<boolean> {
return of(isCartEntryOrGroupVisited);
return isCartEntryOrGroupVisited;
}
}
const DEBOUNCE_TIME: number = 600;
const DEBOUNCE_TIME_DATE: number = 1600;

describe('ConfiguratorAttributeInputFieldComponent', () => {
let component: ConfiguratorAttributeInputFieldComponent;
let fixture: ComponentFixture<ConfiguratorAttributeInputFieldComponent>;
let htmlElem: HTMLElement;
let DEBOUNCE_TIME: number;
const ownerKey = 'theOwnerKey';
const name = 'attributeName';
const groupId = 'theGroupId';
Expand Down Expand Up @@ -72,12 +74,6 @@ describe('ConfiguratorAttributeInputFieldComponent', () => {
provide: ConfiguratorStorefrontUtilsService,
useClass: MockConfigUtilsService,
},
{
provide: FeaturesConfig,
useValue: {
features: { level: '*' },
},
},
],
})
.overrideComponent(ConfiguratorAttributeInputFieldComponent, {
Expand All @@ -104,9 +100,14 @@ describe('ConfiguratorAttributeInputFieldComponent', () => {
component.ownerType = CommonConfigurator.OwnerType.CART_ENTRY;
component.ownerKey = ownerKey;
fixture.detectChanges();
DEBOUNCE_TIME =
defaultConfiguratorUISettingsConfig.productConfigurator
?.updateDebounceTime?.input ?? component['FALLBACK_DEBOUNCE_TIME'];

defaultConfiguratorUISettingsConfig.productConfigurator = {
updateDebounceTime: {
input: DEBOUNCE_TIME,
date: DEBOUNCE_TIME_DATE,
},
};

spyOn(
component['configuratorCommonsService'],
'updateConfiguration'
Expand Down Expand Up @@ -270,6 +271,78 @@ describe('ConfiguratorAttributeInputFieldComponent', () => {
});
});

describe('Accessibility support for attributes of type sap_date', () => {
beforeEach(() => {
component.attribute.uiType = Configurator.UiType.SAP_DATE;
fixture.detectChanges();
});
describe('in case value is empty', () => {
it('should render input element with aria-label attribute that defines an accessible name to label the current element', () => {
CommonConfiguratorTestUtilsService.expectElementContainsA11y(
expect,
htmlElem,
'input',
'form-control',
0,
'aria-label',
'configurator.a11y.valueOfAttributeBlank attribute:' +
component.attribute.label
);
});

it('should render hidden label with aria-label attribute that explains that date picker can be reached by hitting space ', () => {
fixture.detectChanges();
CommonConfiguratorTestUtilsService.expectElementContainsA11y(
expect,
htmlElem,
'label',
'cx-visually-hidden',
0,
'aria-label',
'configurator.a11y.valueOfDateAttributeBlank attribute:' +
component.attribute.label
);
});
});
describe('in case value is present', () => {
beforeEach(() => {
component.attribute.userInput = '2024-12-31';
fixture.detectChanges();
});
it("should contain input element with class name 'form-control' with an 'aria-label' attribute that also mentions the value", () => {
fixture.detectChanges();
CommonConfiguratorTestUtilsService.expectElementContainsA11y(
expect,
htmlElem,
'input',
'form-control',
0,
'aria-label',
'configurator.a11y.valueOfAttributeFull attribute:' +
component.attribute.label +
' value:' +
component.attribute.userInput
);
});

it('should render hidden label with aria-label attribute that explains that date picker can be reached by hitting space ', () => {
fixture.detectChanges();
CommonConfiguratorTestUtilsService.expectElementContainsA11y(
expect,
htmlElem,
'label',
'cx-visually-hidden',
0,
'aria-label',
'configurator.a11y.valueOfDateAttributeFull attribute:' +
component.attribute.label +
' value:' +
component.attribute.userInput
);
});
});
});

describe('isRequired', () => {
it('should tell from attribute if form input required for string and numeric attribute without domain', () => {
expect(component.isRequired).toBe(true);
Expand Down Expand Up @@ -315,4 +388,89 @@ describe('ConfiguratorAttributeInputFieldComponent', () => {
expect(component.isUserInputEmpty).toBe(true);
});
});

describe('compileShowRequiredErrorMessage', () => {
it('should create observable that emits false in case configuration is in initial state', () => {
const falseEmittingObs = cold('a', { a: false });
isCartEntryOrGroupVisited = falseEmittingObs;
component['compileShowRequiredErrorMessage']();
expect(component.showRequiredErrorMessage$).toBeObservable(
falseEmittingObs
);
});
});

describe('calculateDebounceTime', () => {
it('should return debounce time from configuration if available', () => {
expect(component['calculateDebounceTime']()).toBe(DEBOUNCE_TIME);
});

it('should return fallback if nothing available in configuration', () => {
component['config'].productConfigurator = {};
expect(component['calculateDebounceTime']()).toBe(
component['FALLBACK_DEBOUNCE_TIME']
);
});

it('should return debounce time for dates in case provided', () => {
component['debounceForDateActive'] = true;
component.attribute.uiType = Configurator.UiType.SAP_DATE;
expect(component['calculateDebounceTime']()).toBe(DEBOUNCE_TIME_DATE);
});

it('should return zero if debounceForDateActive is false', () => {
component['debounceForDateActive'] = false;
component.attribute.uiType = Configurator.UiType.SAP_DATE;
expect(component['calculateDebounceTime']()).toBe(0);
});

it('should return fallback debounce time for dates in case not provided in configuration', () => {
component['debounceForDateActive'] = true;
component.attribute.uiType = Configurator.UiType.SAP_DATE;
component['config'].productConfigurator = {};
expect(component['calculateDebounceTime']()).toBe(
component['FALLBACK_DEBOUNCE_TIME_DATE']
);
});
});

describe('activateDebounceDate', () => {
it('should activate the debounce time for date input', () => {
expect(component['debounceForDateActive']).toBe(false);
component.activateDebounceDate();
expect(component['debounceForDateActive']).toBe(true);
});
});

describe('inputType', () => {
it('should return "text" for UI type string', () => {
expect(component.inputType).toBe('text');
});

it('should return "date" for UI type sap_date', () => {
component.attribute.uiType = Configurator.UiType.SAP_DATE;
expect(component.inputType).toBe('date');
});

it('should return "date" for UI type DDLB with additional value and validation type sap date', () => {
component.attribute.uiType =
Configurator.UiType.DROPDOWN_ADDITIONAL_INPUT;
component.attribute.validationType = Configurator.ValidationType.SAP_DATE;
expect(component.inputType).toBe('date');
});

it('should return "date" for UI type RB with additional value and validation type sap date', () => {
component.attribute.uiType =
Configurator.UiType.RADIOBUTTON_ADDITIONAL_INPUT;
component.attribute.validationType = Configurator.ValidationType.SAP_DATE;
expect(component.inputType).toBe('date');
});

it('should return "text" for UI type DDLB with additional value and validation type NONE', () => {
component.attribute.uiType =
Configurator.UiType.DROPDOWN_ADDITIONAL_INPUT;
component.attribute.validationType = Configurator.ValidationType.NONE;
expect(component.inputType).toBe('text');
});
});
});
Loading

0 comments on commit 3ca1c6a

Please sign in to comment.