From 8d36d3998c5ae42f5d5d5898cd2de9d36378a1a5 Mon Sep 17 00:00:00 2001 From: rowa-audil Date: Wed, 20 Nov 2019 10:32:50 +0100 Subject: [PATCH] feat(testing): Adds testing module for propagation of an attribute to a components overlay. --- apps/dev/src/app.module.ts | 5 ++ .../autocomplete-demo.component.html | 1 + .../context-dialog-demo.component.html | 1 + apps/dev/src/devapp.component.html | 3 +- .../dev/src/select/select-demo.component.html | 2 +- apps/dev/src/tag/tag-demo.component.html | 1 + .../autocomplete/src/autocomplete-trigger.ts | 13 +++- .../autocomplete/src/autocomplete.spec.ts | 46 ++++++++++++++ .../chart/src/tooltip/chart-tooltip.spec.ts | 27 +++++++- components/chart/src/tooltip/chart-tooltip.ts | 20 +++++- .../src/confirmation-dialog.spec.ts | 29 ++++++++- .../src/confirmation-dialog.ts | 21 ++++++- components/consumption/src/consumption.ts | 14 +++++ .../context-dialog/src/context-dialog.spec.ts | 31 ++++++++++ .../context-dialog/src/context-dialog.ts | 14 +++++ components/core/index.ts | 1 + components/core/src/testing/index.ts | 17 ++++++ .../core/src/testing/ui-test-configuration.ts | 61 +++++++++++++++++++ .../event-chart/src/event-chart.spec.ts | 29 ++++++++- components/event-chart/src/event-chart.ts | 15 +++++ .../filter-field-range-trigger.ts | 2 - .../filter-field/src/filter-field.spec.ts | 3 +- components/select/src/select.spec.ts | 22 ++++++- components/select/src/select.ts | 13 ++++ components/tag/src/tag-add/tag-add.spec.ts | 26 +++++++- components/tag/src/tag-add/tag-add.ts | 18 ++++++ documentation/concepts.json | 5 ++ documentation/concepts.md | 50 +++++++++++++++ documentation/linting.json | 2 +- release.json | 2 +- .../dt-card-direct-children-rule/test.ts.lint | 6 +- .../rules/dt-card-no-empty-rule/test.ts.lint | 2 +- .../dt-tab-content-no-empty-rule/test.ts.lint | 4 +- 33 files changed, 484 insertions(+), 22 deletions(-) create mode 100644 components/core/src/testing/index.ts create mode 100644 components/core/src/testing/ui-test-configuration.ts create mode 100644 documentation/concepts.json create mode 100644 documentation/concepts.md diff --git a/apps/dev/src/app.module.ts b/apps/dev/src/app.module.ts index 923cad735a..5f9dfbcfcd 100644 --- a/apps/dev/src/app.module.ts +++ b/apps/dev/src/app.module.ts @@ -85,6 +85,10 @@ import { ToggleButtonGroupDemo } from './toggle-button-group/toggle-button-group import { TopBarNavigationDemo } from './top-bar-navigation/top-bar-navigation-demo.component'; import { TreeTableDemo } from './tree-table/tree-table-demo.component'; import { DtIconModule } from '@dynatrace/barista-components/icon'; +import { + DT_UI_TEST_CONFIG, + DT_DEFAULT_UI_TEST_CONFIG, +} from '@dynatrace/barista-components/core'; // tslint:disable-next-line: use-component-selector @Component({ template: '' }) @@ -164,6 +168,7 @@ export class NoopRouteComponent {} providers: [ Location, { provide: LocationStrategy, useClass: PathLocationStrategy }, + { provide: DT_UI_TEST_CONFIG, useValue: DT_DEFAULT_UI_TEST_CONFIG }, { provide: STEPPER_GLOBAL_OPTIONS, useValue: { showError: true } }, ], }) diff --git a/apps/dev/src/autocomplete/autocomplete-demo.component.html b/apps/dev/src/autocomplete/autocomplete-demo.component.html index 8aad82ff50..f2746beecd 100644 --- a/apps/dev/src/autocomplete/autocomplete-demo.component.html +++ b/apps/dev/src/autocomplete/autocomplete-demo.component.html @@ -3,6 +3,7 @@ [dtAutocomplete]="auto" [(ngModel)]="value" placeholder="Start typing" + dt-ui-test-id="autocomplete" /> diff --git a/apps/dev/src/context-dialog/context-dialog-demo.component.html b/apps/dev/src/context-dialog/context-dialog-demo.component.html index ff65e2f4fe..a09228cc88 100644 --- a/apps/dev/src/context-dialog/context-dialog-demo.component.html +++ b/apps/dev/src/context-dialog/context-dialog-demo.component.html @@ -2,6 +2,7 @@ aria-label="Open context dialog" aria-label-close-button="Close context dialog" [overlayPanelClass]="panel" + dt-ui-test-id="context-dialog" > diff --git a/apps/dev/src/devapp.component.html b/apps/dev/src/devapp.component.html index 4b5015bd18..ddce70f3e0 100644 --- a/apps/dev/src/devapp.component.html +++ b/apps/dev/src/devapp.component.html @@ -2,7 +2,8 @@
- Barista dev app + + Dynatrace
diff --git a/apps/dev/src/select/select-demo.component.html b/apps/dev/src/select/select-demo.component.html index 1381e804c8..9d781381bd 100644 --- a/apps/dev/src/select/select-demo.component.html +++ b/apps/dev/src/select/select-demo.component.html @@ -1,4 +1,4 @@ - + ThePerfectPour with some very long text even longer longlonglong diff --git a/apps/dev/src/tag/tag-demo.component.html b/apps/dev/src/tag/tag-demo.component.html index 5ecccdadd6..afcba4e746 100644 --- a/apps/dev/src/tag/tag-demo.component.html +++ b/apps/dev/src/tag/tag-demo.component.html @@ -58,6 +58,7 @@ diff --git a/components/autocomplete/src/autocomplete-trigger.ts b/components/autocomplete/src/autocomplete-trigger.ts index 941abdbc00..6110f27536 100644 --- a/components/autocomplete/src/autocomplete-trigger.ts +++ b/components/autocomplete/src/autocomplete-trigger.ts @@ -77,6 +77,9 @@ import { _readKeyCode, stringify, DtFlexibleConnectedPositionStrategy, + dtSetUiTestAttribute, + DT_UI_TEST_CONFIG, + DtUiTestConfiguration, } from '@dynatrace/barista-components/core'; import { DtFormField } from '@dynatrace/barista-components/form-field'; @@ -269,6 +272,9 @@ export class DtAutocompleteTrigger @Optional() private _platform?: Platform, /** @breaking-change 7.0.0 Make the _overlayContainer non optional. */ @Optional() private _overlayContainer?: OverlayContainer, + @Optional() + @Inject(DT_UI_TEST_CONFIG) + private _config?: DtUiTestConfiguration, ) { // tslint:disable-next-line:strict-type-predicates if (typeof window !== 'undefined') { @@ -433,7 +439,12 @@ export class DtAutocompleteTrigger if (!this._overlayRef) { this._overlayRef = this._overlay.create(this._getOverlayConfig()); - + dtSetUiTestAttribute( + this._overlayRef.overlayElement, + this._overlayRef.overlayElement.id, + this._element, + this._config, + ); this._overlayRef.keydownEvents().subscribe(event => { const keyCode = _readKeyCode(event); // Close when pressing ESCAPE or ALT + UP_ARROW, based on the a11y guidelines. diff --git a/components/autocomplete/src/autocomplete.spec.ts b/components/autocomplete/src/autocomplete.spec.ts index e272be7b53..e99476cb8c 100644 --- a/components/autocomplete/src/autocomplete.spec.ts +++ b/components/autocomplete/src/autocomplete.spec.ts @@ -61,6 +61,8 @@ import { import { DtOption, DtOptionSelectionChange, + DT_UI_TEST_CONFIG, + DtUiTestConfiguration, } from '@dynatrace/barista-components/core'; import { DtFormField, @@ -82,6 +84,13 @@ describe('DtAutocomplete', () => { let overlayContainerElement: HTMLElement; let zone: MockNgZone; + const overlayConfig: DtUiTestConfiguration = { + attributeName: 'dt-ui-test-id', + constructOverlayAttributeValue(attributeName: string): string { + return `${attributeName}-overlay`; + }, + }; + // Creates a test component fixture. function createComponent( component: Type, @@ -99,6 +108,7 @@ describe('DtAutocomplete', () => { declarations: [component], providers: [ { provide: NgZone, useFactory: () => (zone = new MockNgZone()) }, + { provide: DT_UI_TEST_CONFIG, useValue: overlayConfig }, ...providers, ], }); @@ -1568,6 +1578,22 @@ describe('DtAutocomplete', () => { expect(panel.classList).toContain('class-two'); })); }); + describe('propagate attribute to overlay', () => { + it('should propagate attribute to overlay when `dt-ui-test-id` is provided', () => { + const fixture: ComponentFixture = createComponent( + PropagateAttribute, + ); + fixture.detectChanges(); + const trigger = fixture.componentInstance.trigger; + trigger.openPanel(); + const overlay = overlayContainerElement.querySelector( + '.dt-autocomplete-panel', + )!.parentElement; + expect(overlay!.outerHTML).toContain( + 'dt-ui-test-id="autocomplete-overlay"', + ); + }); + }); }); @Component({ @@ -1888,3 +1914,23 @@ class DynamicallyChangingAutocomplete { @ViewChild(DtAutocompleteTrigger, { static: false }) trigger: DtAutocompleteTrigger; } + +@Component({ + template: ` + + + First + + `, +}) +class PropagateAttribute { + @ViewChild(DtAutocompleteTrigger, { static: false }) + trigger: DtAutocompleteTrigger; +} diff --git a/components/chart/src/tooltip/chart-tooltip.spec.ts b/components/chart/src/tooltip/chart-tooltip.spec.ts index 5203ae2574..08d2c38d24 100644 --- a/components/chart/src/tooltip/chart-tooltip.spec.ts +++ b/components/chart/src/tooltip/chart-tooltip.spec.ts @@ -44,6 +44,10 @@ import { DtThemingModule } from '@dynatrace/barista-components/theming'; import { createComponent } from '@dynatrace/barista-components/testing'; import { MockIntersectionObserver } from '@dynatrace/barista-components/testing/mock'; import { DtChartTooltipData } from '../highcharts/highcharts-tooltip-types'; +import { + DT_UI_TEST_CONFIG, + DtUiTestConfiguration, +} from '@dynatrace/barista-components/core'; describe('DtChartTooltip', () => { let overlayContainer: OverlayContainer; @@ -51,6 +55,12 @@ describe('DtChartTooltip', () => { let chartComponent: DtChart; let fixture: ComponentFixture; const mockIntersectionObserver = new MockIntersectionObserver(); + const overlayConfig: DtUiTestConfiguration = { + attributeName: 'dt-ui-test-id', + constructOverlayAttributeValue(attributeName: string): string { + return `${attributeName}-overlay`; + }, + }; beforeEach(async(() => { TestBed.configureTestingModule({ @@ -62,6 +72,7 @@ describe('DtChartTooltip', () => { NoopAnimationsModule, ], declarations: [ChartTest], + providers: [{ provide: DT_UI_TEST_CONFIG, useValue: overlayConfig }], }); TestBed.compileComponents(); @@ -163,13 +174,27 @@ describe('DtChartTooltip', () => { expect(overlayContainerElement.textContent).toContain('54321'); }); }); + describe('propagate attribute to overlay', () => { + it('should propagate attribute to overlay when `dt-ui-test-id` is provided', fakeAsync(() => { + mockIntersectionObserver.mockAllIsIntersecting(true); + chartComponent._highChartsTooltipOpened$.next({ + data: DUMMY_TOOLTIP_DATA_LINE_SERIES, + }); + fixture.detectChanges(); + tick(); + flush(); + expect(overlayContainerElement.innerHTML).toContain( + 'dt-ui-test-id="tooltip-overlay', + ); + })); + }); }); @Component({ selector: 'dt-line', template: ` - + diff --git a/components/chart/src/tooltip/chart-tooltip.ts b/components/chart/src/tooltip/chart-tooltip.ts index 72663e8c5d..d365cfbbd9 100644 --- a/components/chart/src/tooltip/chart-tooltip.ts +++ b/components/chart/src/tooltip/chart-tooltip.ts @@ -30,10 +30,18 @@ import { TemplateRef, ViewContainerRef, ViewEncapsulation, + ElementRef, + Inject, + Optional, } from '@angular/core'; import { Subject, Subscription } from 'rxjs'; -import { isDefined } from '@dynatrace/barista-components/core'; +import { + isDefined, + DT_UI_TEST_CONFIG, + DtUiTestConfiguration, + dtSetUiTestAttribute, +} from '@dynatrace/barista-components/core'; import { DtChart } from '../chart'; import { DtChartTooltipData } from '../highcharts/highcharts-tooltip-types'; @@ -86,6 +94,10 @@ export class DtChartTooltip implements OnDestroy { private _overlay: Overlay, private _viewContainerRef: ViewContainerRef, private _changeDetectorRef: ChangeDetectorRef, + private _elementRef: ElementRef, + @Optional() + @Inject(DT_UI_TEST_CONFIG) + private _config?: DtUiTestConfiguration, ) {} ngOnDestroy(): void { @@ -133,6 +145,12 @@ export class DtChartTooltip implements OnDestroy { overlayRef.attach(this._portal); this._overlayRef = overlayRef; + dtSetUiTestAttribute( + this._overlayRef.overlayElement, + this._overlayRef.overlayElement.id, + this._elementRef, + this._config, + ); } } diff --git a/components/confirmation-dialog/src/confirmation-dialog.spec.ts b/components/confirmation-dialog/src/confirmation-dialog.spec.ts index e894c7a4a9..cabd710c07 100644 --- a/components/confirmation-dialog/src/confirmation-dialog.spec.ts +++ b/components/confirmation-dialog/src/confirmation-dialog.spec.ts @@ -33,17 +33,27 @@ import { } from './confirmation-dialog-constants'; import { DtConfirmationDialogModule } from './confirmation-dialog-module'; import { DtConfirmationDialog } from './confirmation-dialog'; +import { + DT_UI_TEST_CONFIG, + DtUiTestConfiguration, +} from '@dynatrace/barista-components/core'; describe('ConfirmationDialogComponent', () => { const UP = 'translateY(0)'; const DOWN = 'translateY(100%)'; let overlayContainerElement: HTMLElement; + const overlayConfig: DtUiTestConfiguration = { + attributeName: 'dt-ui-test-id', + constructOverlayAttributeValue(attributeName: string): string { + return `${attributeName}-overlay`; + }, + }; beforeEach(fakeAsync(() => { TestBed.configureTestingModule({ imports: [NoopAnimationsModule, DtConfirmationDialogModule], - providers: [], + providers: [{ provide: DT_UI_TEST_CONFIG, useValue: overlayConfig }], declarations: [TestComponent, DynamicStates, TwoDialogsComponent], }).compileComponents(); })); @@ -210,6 +220,17 @@ describe('ConfirmationDialogComponent', () => { expect(dialog2State.textContent!.trim()).toEqual('state2'); })); }); + describe('propagate attribute to overlay', () => { + it('should propagate attribute to overlay when `dt-ui-test-id` is provided', fakeAsync(() => { + const fixture = TestBed.createComponent(TestComponent); + fixture.componentInstance.testState = 'missingState'; + fixture.detectChanges(); + tick(); + expect(overlayContainerElement.innerHTML).toContain( + 'dt-ui-test-id="confirmation-dialog-overlay', + ); + })); + }); }); function testTextContent( @@ -242,7 +263,11 @@ function getState(oc: HTMLElement, name: string): HTMLElement { @Component({ selector: 'dt-test-component', template: ` - + state1 diff --git a/components/confirmation-dialog/src/confirmation-dialog.ts b/components/confirmation-dialog/src/confirmation-dialog.ts index 69c5f2cb03..dbd870c22b 100644 --- a/components/confirmation-dialog/src/confirmation-dialog.ts +++ b/components/confirmation-dialog/src/confirmation-dialog.ts @@ -39,10 +39,19 @@ import { ViewContainerRef, ViewEncapsulation, isDevMode, + Optional, + Inject, + ElementRef, } from '@angular/core'; import { Subscription } from 'rxjs'; -import { DtLogger, DtLoggerFactory } from '@dynatrace/barista-components/core'; +import { + DtLogger, + DtLoggerFactory, + DtUiTestConfiguration, + dtSetUiTestAttribute, + DT_UI_TEST_CONFIG, +} from '@dynatrace/barista-components/core'; import { DT_CONFIRMATION_BACKDROP_ACTIVE_OPACITY, @@ -136,6 +145,10 @@ export class DtConfirmationDialog private _overlay: Overlay, private _viewContainerRef: ViewContainerRef, private _changeDetectorRef: ChangeDetectorRef, + private _elementRef: ElementRef, + @Optional() + @Inject(DT_UI_TEST_CONFIG) + private _config?: DtUiTestConfiguration, ) {} ngAfterContentChecked(): void { @@ -236,6 +249,12 @@ export class DtConfirmationDialog }, positionStrategy, }); + dtSetUiTestAttribute( + overlayRef.overlayElement, + overlayRef.overlayElement.id, + this._elementRef, + this._config, + ); const containerPortal = new TemplatePortal<{}>( this._templateRef, this._viewContainerRef, diff --git a/components/consumption/src/consumption.ts b/components/consumption/src/consumption.ts index 45d1f0ba57..c954c731f6 100644 --- a/components/consumption/src/consumption.ts +++ b/components/consumption/src/consumption.ts @@ -35,6 +35,8 @@ import { TemplateRef, ViewContainerRef, ViewEncapsulation, + Optional, + Inject, } from '@angular/core'; import { @@ -42,6 +44,9 @@ import { isDefined, mixinColor, _readKeyCode, + DT_UI_TEST_CONFIG, + DtUiTestConfiguration, + dtSetUiTestAttribute, } from '@dynatrace/barista-components/core'; import { DtConsumptionOverlay } from './consumption-directives'; @@ -143,6 +148,9 @@ export class DtConsumption extends _DtConsumption private readonly _viewContainerRef: ViewContainerRef, private readonly _changeDetectorRef: ChangeDetectorRef, private _focusMonitor: FocusMonitor, + @Optional() + @Inject(DT_UI_TEST_CONFIG) + private _config?: DtUiTestConfiguration, ) { super(_elementRef); this._focusMonitor.monitor(this._elementRef); @@ -179,6 +187,12 @@ export class DtConsumption extends _DtConsumption // Note: each OverlayConfig can only be used for one overlay instance this._overlayRef = this._overlay.create(this._createOverlayConfig()); + dtSetUiTestAttribute( + this._overlayRef.overlayElement, + this._overlayRef.overlayElement.id, + this._elementRef, + this._config, + ); this._overlayRef.attach(portal); } } diff --git a/components/context-dialog/src/context-dialog.spec.ts b/components/context-dialog/src/context-dialog.spec.ts index 29a200cd7e..9346115cac 100644 --- a/components/context-dialog/src/context-dialog.spec.ts +++ b/components/context-dialog/src/context-dialog.spec.ts @@ -43,9 +43,20 @@ import { createComponent, dispatchKeyboardEvent, } from '@dynatrace/barista-components/testing'; +import { + DT_UI_TEST_CONFIG, + DtUiTestConfiguration, +} from '@dynatrace/barista-components/core'; describe('DtContextDialog', () => { let overlayContainer: OverlayContainer; + let overlayContainerElement: HTMLElement; + const overlayConfig: DtUiTestConfiguration = { + attributeName: 'dt-ui-test-id', + constructOverlayAttributeValue(attributeName: string): string { + return `${attributeName}-overlay`; + }, + }; // tslint:disable-next-line:no-any function configureDtContextDialogTestingModule(declarations: any[]): void { @@ -56,10 +67,12 @@ describe('DtContextDialog', () => { DtIconModule.forRoot({ svgIconLocation: `{{name}}.svg` }), ], declarations, + providers: [{ provide: DT_UI_TEST_CONFIG, useValue: overlayConfig }], }).compileComponents(); inject([OverlayContainer], (oc: OverlayContainer) => { overlayContainer = oc; + overlayContainerElement = oc.getContainerElement(); })(); } @@ -345,6 +358,23 @@ describe('DtContextDialog', () => { ).toBeDefined(); })); }); + describe('propagate attribute to overlay', () => { + let fixture; + beforeEach(fakeAsync(() => { + fixture = TestBed.createComponent(BasicContextDialog); + fixture.detectChanges(); + })); + // tslint:disable-next-line: dt-no-focused-tests + it('should propagate attribute to overlay when `dt-ui-test-id` is provided', fakeAsync(() => { + const contextDialog = fixture.componentInstance.contextDialog; + contextDialog.open(); + fixture.detectChanges(); + tick(); + expect(overlayContainerElement.innerHTML).toContain( + 'dt-ui-test-id="context-dialog-overlay"', + ); + })); + }); }); }); @@ -370,6 +400,7 @@ describe('DtContextDialog', () => { [tabIndex]="tabIndexOverride" [disabled]="disabled" [overlayPanelClass]="panelClass" + dt-ui-test-id="context-dialog" >

Some cool content

diff --git a/components/context-dialog/src/context-dialog.ts b/components/context-dialog/src/context-dialog.ts index d9a85190b3..030a6c45f3 100644 --- a/components/context-dialog/src/context-dialog.ts +++ b/components/context-dialog/src/context-dialog.ts @@ -54,6 +54,9 @@ import { mixinDisabled, mixinTabIndex, _readKeyCode, + DT_UI_TEST_CONFIG, + DtUiTestConfiguration, + dtSetUiTestAttribute, } from '@dynatrace/barista-components/core'; import { DtContextDialogTrigger } from './context-dialog-trigger'; @@ -200,6 +203,11 @@ export class DtContextDialog extends _DtContextDialogMixinBase @Attribute('tabindex') tabIndex: string, // tslint:disable-next-line: no-any @Optional() @Inject(DOCUMENT) private _document: any, + /** @breaking-change: `_elementRef` will be mandatory with version 7.0.0 */ + private _elementRef?: ElementRef, + @Optional() + @Inject(DT_UI_TEST_CONFIG) + private _config?: DtUiTestConfiguration, ) { super(); this.tabIndex = parseInt(tabIndex, 10) || 0; @@ -309,6 +317,12 @@ export class DtContextDialog extends _DtContextDialogMixinBase backdropClass: 'cdk-overlay-transparent-backdrop', hasBackdrop: true, }); + dtSetUiTestAttribute( + this._overlayRef.overlayElement, + this._overlayRef.overlayElement.id, + this._elementRef, + this._config, + ); this._overlayRef .backdropClick() .pipe(takeUntil(this._destroy)) diff --git a/components/core/index.ts b/components/core/index.ts index 1de018b773..6f3b93d60d 100644 --- a/components/core/index.ts +++ b/components/core/index.ts @@ -24,3 +24,4 @@ export * from './src/indicator/index'; export * from './src/tree/index'; export * from './src/animations/index'; export * from './src/overlay/index'; +export * from './src/testing/index'; diff --git a/components/core/src/testing/index.ts b/components/core/src/testing/index.ts new file mode 100644 index 0000000000..5e2270bf10 --- /dev/null +++ b/components/core/src/testing/index.ts @@ -0,0 +1,17 @@ +/** + * @license + * Copyright 2020 Dynatrace LLC + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './ui-test-configuration'; diff --git a/components/core/src/testing/ui-test-configuration.ts b/components/core/src/testing/ui-test-configuration.ts new file mode 100644 index 0000000000..34c578ff4c --- /dev/null +++ b/components/core/src/testing/ui-test-configuration.ts @@ -0,0 +1,61 @@ +/** + * @license + * Copyright 2020 Dynatrace LLC + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { InjectionToken, ElementRef } from '@angular/core'; +import { coerceElement } from '@angular/cdk/coercion'; + +/** Interface for Injectiontoken to set UI-test attribute to overlay Container */ +export interface DtUiTestConfiguration { + attributeName: string; + constructOverlayAttributeValue(uiTestId: string, id: number): string; +} + +/** Default configuration for setting the UI-test attribute */ +export const DT_DEFAULT_UI_TEST_CONFIG: DtUiTestConfiguration = { + attributeName: 'dt-ui-test-id', + // tslint:disable-next-line: typedef + constructOverlayAttributeValue(uiTestId: string, id = 0): string { + return `${uiTestId}-overlay-${id}`; + }, +}; + +/** Injectiontoken of the UI-test configuration */ +export const DT_UI_TEST_CONFIG = new InjectionToken( + 'DT_UI_TEST_CONFIGURATION', +); + +/** Sets the UI-test attribute to the overlay container */ +export function dtSetUiTestAttribute( + overlay: Element, + overlayId: string | null, + componentElement?: ElementRef | Element, + config?: DtUiTestConfiguration, +): void { + if (componentElement && config) { + const element = coerceElement(componentElement); + if (overlay && element.hasAttribute(config.attributeName) && overlayId) { + // Angular CDK hardcoded the ID for the overlay with `cdk-overlay-{uniqueIndex}` + const index = parseInt(overlayId.replace('cdk-overlay-', '')); + overlay.setAttribute( + config.attributeName, + config.constructOverlayAttributeValue( + element.getAttribute(config.attributeName)!, + index, + ), + ); + } + } +} diff --git a/components/event-chart/src/event-chart.spec.ts b/components/event-chart/src/event-chart.spec.ts index 6f0b249ef4..cc2ce78dfa 100644 --- a/components/event-chart/src/event-chart.spec.ts +++ b/components/event-chart/src/event-chart.spec.ts @@ -27,6 +27,10 @@ import { DtEventChartModule } from '@dynatrace/barista-components/event-chart'; import { dispatchFakeEvent } from '@dynatrace/barista-components/testing'; import { DtEventChart } from './event-chart'; import { DtEventChartSelectedEvent } from './event-chart-directives'; +import { + DT_UI_TEST_CONFIG, + DtUiTestConfiguration, +} from '@dynatrace/barista-components/core'; /** Gets the rendered merged numbering. */ function getRenderedMergedTextLabels(fixture: ComponentFixture): string[] { @@ -100,6 +104,12 @@ function getLaneLabels(fixture: ComponentFixture): string[] { describe('DtEventChart', () => { let overlayContainer: OverlayContainer; let overlayContainerElement: HTMLElement; + const overlayConfig: DtUiTestConfiguration = { + attributeName: 'dt-ui-test-id', + constructOverlayAttributeValue(attributeName: string): string { + return `${attributeName}-overlay`; + }, + }; beforeEach(() => { TestBed.configureTestingModule({ @@ -109,6 +119,7 @@ describe('DtEventChart', () => { EventChartStaticDataWithLegendAndOverlay, EventChartDynamicData, ], + providers: [{ provide: DT_UI_TEST_CONFIG, useValue: overlayConfig }], }); TestBed.compileComponents(); }); @@ -340,6 +351,22 @@ describe('DtEventChart', () => { 'dt-event-chart-event-selected', ); }); + + it('should propagate attribute to overlay if `dt-ui-test-id` is provided', () => { + const firstBubble = fixture.debugElement.query( + By.css('.dt-event-chart-event'), + ); + dispatchFakeEvent(firstBubble.nativeElement, 'mouseenter'); + fixture.detectChanges(); + + const overlayPane = overlayContainerElement.querySelector( + '.dt-event-chart-overlay-panel', + ); + + expect(overlayPane!.outerHTML).toContain( + 'dt-ui-test-id="event-chart-overlay"', + ); + }); }); describe('with dynamic data', () => { @@ -647,7 +674,7 @@ class EventChartStaticData { @Component({ selector: 'dt-test-app', template: ` - + implements AfterContentInit, OnInit, OnDestroy { // tslint:disable-next-line: no-any @Inject(DOCUMENT) private _document: any, private _platform: Platform, + /** @breaking-change: `_elementRef` will be mandatory with version 7.0.0 */ + private _elementRef?: ElementRef, + @Optional() + @Inject(DT_UI_TEST_CONFIG) + private _config?: DtUiTestConfiguration, ) {} ngOnInit(): void { @@ -505,6 +514,12 @@ export class DtEventChart implements AfterContentInit, OnInit, OnDestroy { if (!this._overlayRef.hasAttached()) { this._overlayRef.attach(this._portal); } + dtSetUiTestAttribute( + this._overlayRef.overlayElement, + this._overlayRef.overlayElement.id, + this._elementRef, + this._config, + ); } /** Update the overlay position and the implicit context. */ diff --git a/components/filter-field/src/filter-field-range/filter-field-range-trigger.ts b/components/filter-field/src/filter-field-range/filter-field-range-trigger.ts index 8abc0462fb..f426e09b3f 100644 --- a/components/filter-field/src/filter-field-range/filter-field-range-trigger.ts +++ b/components/filter-field/src/filter-field-range/filter-field-range-trigger.ts @@ -238,7 +238,6 @@ export class DtFilterFieldRangeTrigger implements OnDestroy { private _attachOverlay(): void { if (!this._overlayRef) { this._overlayRef = this._overlay.create(this._getOverlayConfig()); - this._overlayRef.keydownEvents().subscribe(event => { const keyCode = _readKeyCode(event); // Close when pressing ESCAPE or ALT + UP_ARROW, based on the a11y guidelines. @@ -248,7 +247,6 @@ export class DtFilterFieldRangeTrigger implements OnDestroy { } }); } - if (this._overlayRef && !this._overlayRef.hasAttached()) { this._overlayRef.attach(this._range._portal); this._closingActionsSubscription = this._subscribeToClosingActions(); diff --git a/components/filter-field/src/filter-field.spec.ts b/components/filter-field/src/filter-field.spec.ts index 3a2f3b2c13..20d5c0fb31 100644 --- a/components/filter-field/src/filter-field.spec.ts +++ b/components/filter-field/src/filter-field.spec.ts @@ -42,10 +42,11 @@ import { DtFilterFieldDefaultDataSourceType, } from '@dynatrace/barista-components/filter-field'; import { DtIconModule } from '@dynatrace/barista-components/icon'; + import { dispatchFakeEvent, - createComponent, dispatchKeyboardEvent, + createComponent, MockNgZone, typeInElement, wrappedErrorMessage, diff --git a/components/select/src/select.spec.ts b/components/select/src/select.spec.ts index b592040abc..c775ea0ae8 100644 --- a/components/select/src/select.spec.ts +++ b/components/select/src/select.spec.ts @@ -62,6 +62,8 @@ import { DtOption, DtOptionSelectionChange, ErrorStateMatcher, + DT_UI_TEST_CONFIG, + DtUiTestConfiguration, } from '@dynatrace/barista-components/core'; import { DtFormFieldModule } from '@dynatrace/barista-components/form-field'; import { DtIconModule } from '@dynatrace/barista-components/icon'; @@ -74,14 +76,20 @@ import { dispatchEvent, dispatchFakeEvent, dispatchKeyboardEvent, + createComponent, createKeyboardEvent, wrappedErrorMessage, - createComponent, } from '@dynatrace/barista-components/testing'; describe('DtSelect', () => { let overlayContainer: OverlayContainer; let overlayContainerElement: HTMLElement; + const overlayConfig: DtUiTestConfiguration = { + attributeName: 'dt-ui-test-id', + constructOverlayAttributeValue(attributeName: string): string { + return `${attributeName}-overlay`; + }, + }; function configureDtSelectTestingModule(declarations: any[]): void { TestBed.configureTestingModule({ @@ -95,6 +103,7 @@ describe('DtSelect', () => { DtIconModule.forRoot({ svgIconLocation: `{{name}}.svg` }), ], declarations, + providers: [{ provide: DT_UI_TEST_CONFIG, useValue: overlayConfig }], }).compileComponents(); inject([OverlayContainer], (oc: OverlayContainer) => { @@ -709,6 +718,16 @@ describe('DtSelect', () => { expect(overlayContainerElement.textContent).toContain('Tacos'); })); + it('should propagate attribute to overlay if `dt-ui-test-id` is provided', fakeAsync(() => { + trigger.click(); + fixture.detectChanges(); + flush(); + + expect(overlayContainerElement.innerHTML).toContain( + 'dt-ui-test-id="select-overlay"', + ); + })); + it('should close the panel when an item is clicked', fakeAsync(() => { trigger.click(); fixture.detectChanges(); @@ -2018,6 +2037,7 @@ describe('DtSelect', () => { [aria-label]="ariaLabel" [aria-labelledby]="ariaLabelledby" [panelClass]="panelClass" + dt-ui-test-id="select" > extends _DtSelectMixinBase @Self() @Optional() public ngControl: NgControl, @Attribute('tabindex') tabIndex: string, private _focusMonitor: FocusMonitor, + @Optional() + @Inject(DT_UI_TEST_CONFIG) + private _config?: DtUiTestConfiguration, ) { super( elementRef, @@ -667,6 +674,12 @@ export class DtSelect extends _DtSelectMixinBase this._changeDetectorRef.detectChanges(); this.panel.nativeElement.scrollTop = this._scrollTop; }); + dtSetUiTestAttribute( + this.overlayDir.overlayRef.overlayElement, + this.overlayDir.overlayRef.overlayElement.id, + this._elementRef, + this._config, + ); } /** diff --git a/components/tag/src/tag-add/tag-add.spec.ts b/components/tag/src/tag-add/tag-add.spec.ts index eb95183064..846746cfa2 100644 --- a/components/tag/src/tag-add/tag-add.spec.ts +++ b/components/tag/src/tag-add/tag-add.spec.ts @@ -36,6 +36,10 @@ import { createComponent, dispatchKeyboardEvent, } from '@dynatrace/barista-components/testing'; +import { + DT_UI_TEST_CONFIG, + DtUiTestConfiguration, +} from '@dynatrace/barista-components/core'; describe('DtTagAdd', () => { let fixture: ComponentFixture; @@ -45,6 +49,12 @@ describe('DtTagAdd', () => { let overlayContainer: OverlayContainer; let overlayContainerElement: HTMLElement; + const overlayConfig: DtUiTestConfiguration = { + attributeName: 'dt-ui-test-id', + constructOverlayAttributeValue(attributeName: string): string { + return `${attributeName}-overlay`; + }, + }; beforeEach(() => { TestBed.configureTestingModule({ @@ -54,6 +64,7 @@ describe('DtTagAdd', () => { DtTagModule, ], declarations: [DtTagComponent], + providers: [{ provide: DT_UI_TEST_CONFIG, useValue: overlayConfig }], }); TestBed.compileComponents(); @@ -171,6 +182,15 @@ describe('DtTagAdd', () => { expect(addTagInstance._showOverlay).toBe(false); })); + it('should propagate attribute to overlay if `dt-ui-test-id` is provided', fakeAsync(() => { + addTagInstance.open(); + fixture.detectChanges(); + flush(); + + expect(addTagInstance._showOverlay).toBe(true); + expect(overlayContainerElement.innerHTML).toContain('dt-ui-test-id'); + })); + describe('keyevent tests', () => { it('should close Overlay when ESCAPE is pressed', () => { addTagInstance.open(); @@ -192,7 +212,11 @@ describe('DtTagAdd', () => { selector: 'dt-test-app', template: ` {{ tag }} - + `, }) class DtTagComponent implements OnInit { diff --git a/components/tag/src/tag-add/tag-add.ts b/components/tag/src/tag-add/tag-add.ts index 63ef38bf93..3bacbeafc6 100644 --- a/components/tag/src/tag-add/tag-add.ts +++ b/components/tag/src/tag-add/tag-add.ts @@ -31,11 +31,18 @@ import { ViewChild, ViewChildren, ViewEncapsulation, + Optional, + Inject, } from '@angular/core'; import { Subject } from 'rxjs'; import { switchMap, take, takeUntil } from 'rxjs/operators'; import { DtInput } from '@dynatrace/barista-components/input'; +import { + DtUiTestConfiguration, + DT_UI_TEST_CONFIG, + dtSetUiTestAttribute, +} from '@dynatrace/barista-components/core'; @Component({ selector: 'dt-tag-add', @@ -127,6 +134,11 @@ export class DtTagAdd implements AfterViewInit, OnDestroy { constructor( private _changeDetectorRef: ChangeDetectorRef, private _zone: NgZone, + @Optional() + @Inject(DT_UI_TEST_CONFIG) + private _config?: DtUiTestConfiguration, + /** @breaking-change: `_elementRef` will be mandatory with version 7.0.0 */ + private _elementRef?: ElementRef, ) {} ngAfterViewInit(): void { @@ -153,6 +165,12 @@ export class DtTagAdd implements AfterViewInit, OnDestroy { this._overlayDir.positionChange.pipe(take(1)).subscribe(() => { this._panel.nativeElement.scrollTop = 0; }); + dtSetUiTestAttribute( + this._overlayDir.overlayRef.overlayElement, + this._overlayDir.overlayRef.overlayElement.id, + this._elementRef, + this._config, + ); } /** Opens the tag add Overlay by setting showOverlay to true. */ diff --git a/documentation/concepts.json b/documentation/concepts.json new file mode 100644 index 0000000000..64fc077aaf --- /dev/null +++ b/documentation/concepts.json @@ -0,0 +1,5 @@ +{ + "title": "Concepts", + "order": 6, + "tags": ["documentation", "angular", "testing", "ui"] +} diff --git a/documentation/concepts.md b/documentation/concepts.md new file mode 100644 index 0000000000..19b107aed0 --- /dev/null +++ b/documentation/concepts.md @@ -0,0 +1,50 @@ +# Concepts + +## UI Testing + +### Propagating Attributes to Component Overlays + +In order to test the overlays of the Barista components you'll have to provide +the `DT_UI_TEST_CONFIG` Token into your application's module. You can use the +default configuration `DT_DEFAULT_UI_TEST_CONFIG` which will set the attribute's +value to `[attribute-value]-overlay-[index]`. + +#### Example: Providing in app.module with default configuration + +```ts +@NgModule({ + imports: [BrowserModule], + declaration: [AppComponent], + providers: [ + { + provide: DT_UI_TEST_CONFIG, useValue: DT_DEFAULT_UI_TEST_CONFIG + } + ] + bootstrap: [AppComponent] +}) +``` + +#### Example: Providing in app.module with custom configuration + +You can provide your own custom configuration. Use the interface +`DtUiTestConfiguration` for that. + +```ts +const customUiTestConfig: DtUiTestConfiguration = { + attributeName: 'testid', + constructOverlayAttributeValue(attributeName: string): string { + return `${attributeName}-overlay` + } +} + +@NgModule({ + imports: [BrowserModule], + declaration: [AppComponent], + providers: [ + { + provide: DT_UI_TEST_CONFIG, useValue: customUiTestConfig + } + ] + bootstrap: [AppComponent] +}) +``` diff --git a/documentation/linting.json b/documentation/linting.json index 8b20a121b8..89bc5c52f1 100644 --- a/documentation/linting.json +++ b/documentation/linting.json @@ -1,7 +1,7 @@ { "title": "Linting", "description": "We provide a set of custom TSLint rules to help you using the Angular components correctly as intended.", - "order": 6, + "order": 7, "tags": [ "tslint", "linting", diff --git a/release.json b/release.json index 8255dc3e8d..e6ba0ce2fc 100644 --- a/release.json +++ b/release.json @@ -2,6 +2,6 @@ "title": "Releasing", "description": "At the end of every sprint a new version of the Angular components library is released. Releasing is done by the DesignOps team.", "public": false, - "order": 7, + "order": 8, "tags": ["releasing", "release", "how to", "guideline"] } diff --git a/tools/linting/src/test/rules/dt-card-direct-children-rule/test.ts.lint b/tools/linting/src/test/rules/dt-card-direct-children-rule/test.ts.lint index 632140843d..b18ec25cf5 100644 --- a/tools/linting/src/test/rules/dt-card-direct-children-rule/test.ts.lint +++ b/tools/linting/src/test/rules/dt-card-direct-children-rule/test.ts.lint @@ -36,18 +36,18 @@ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [The following elements must be direct children of a dt-card: dt-card-subtitle] - Cluster utilization + Cluster utilization ({{totalNodeCount | dtCount}} {totalNodeCount, plural, =1 {cluster node} other {cluster nodes}})
- CPU {{mode | lowercase}} + CPU {{mode | lowercase}}
- Memory {{mode | lowercase}} + Memory {{mode | lowercase}}
diff --git a/tools/linting/src/test/rules/dt-card-no-empty-rule/test.ts.lint b/tools/linting/src/test/rules/dt-card-no-empty-rule/test.ts.lint index e55c73e27f..1e007fb0cd 100644 --- a/tools/linting/src/test/rules/dt-card-no-empty-rule/test.ts.lint +++ b/tools/linting/src/test/rules/dt-card-no-empty-rule/test.ts.lint @@ -14,7 +14,7 @@ ~~~~~~~~~~~~~~~~~~~~~~~~~~ [A dt-card must always contain content apart from title, subtitle, icon and actions.] - No events + No events diff --git a/tools/linting/src/test/rules/dt-tab-content-no-empty-rule/test.ts.lint b/tools/linting/src/test/rules/dt-tab-content-no-empty-rule/test.ts.lint index 12209a38c8..d205688535 100644 --- a/tools/linting/src/test/rules/dt-tab-content-no-empty-rule/test.ts.lint +++ b/tools/linting/src/test/rules/dt-tab-content-no-empty-rule/test.ts.lint @@ -8,14 +8,14 @@

Traffic

- + ~~~~~~~~~~~~~~~~~~~~~~~~~~ [A dtTabContent must always contain content.]