Skip to content

feat(datepicker/testing): add test harnesses for the datepicker module #20219

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 13, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/material/config.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ entryPoints = [
"core",
"core/testing",
"datepicker",
"datepicker/testing",
"dialog",
"dialog/testing",
"divider",
Expand Down
4 changes: 2 additions & 2 deletions src/material/datepicker/date-range-input-parts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ const _MatDateRangeInputBase:
@Directive({
selector: 'input[matStartDate]',
host: {
'class': 'mat-date-range-input-inner',
'class': 'mat-start-date mat-date-range-input-inner',
'[disabled]': 'disabled',
'(input)': '_onInput($event.target.value)',
'(change)': '_onChange()',
Expand Down Expand Up @@ -265,7 +265,7 @@ export class MatStartDate<D> extends _MatDateRangeInputBase<D> implements CanUpd
@Directive({
selector: 'input[matEndDate]',
host: {
'class': 'mat-date-range-input-inner',
'class': 'mat-end-date mat-date-range-input-inner',
'[disabled]': 'disabled',
'(input)': '_onInput($event.target.value)',
'(change)': '_onChange()',
Expand Down
4 changes: 4 additions & 0 deletions src/material/datepicker/date-range-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,14 @@ let nextUniqueId = 0;
host: {
'class': 'mat-date-range-input',
'[class.mat-date-range-input-hide-placeholders]': '_shouldHidePlaceholders()',
'[class.mat-date-range-input-required]': 'required',
'[attr.id]': 'null',
'role': 'group',
'[attr.aria-labelledby]': '_getAriaLabelledby()',
'[attr.aria-describedby]': '_ariaDescribedBy',
// Used by the test harness to tie this input to its calendar. We can't depend on
// `aria-owns` for this, because it's only defined while the calendar is open.
'[attr.data-mat-calendar]': 'rangePicker ? rangePicker.id : null',
},
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
Expand Down
6 changes: 5 additions & 1 deletion src/material/datepicker/datepicker-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,9 @@ export abstract class MatDatepickerBase<C extends MatDatepickerControl<D>, S,
/** The element that was focused before the datepicker was opened. */
private _focusedElementBeforeOpen: HTMLElement | null = null;

/** Unique class that will be added to the backdrop so that the test harnesses can look it up. */
private _backdropHarnessClass = `${this.id}-backdrop`;

/** The input element this datepicker is associated with. */
_datepickerInput: C;

Expand Down Expand Up @@ -516,6 +519,7 @@ export abstract class MatDatepickerBase<C extends MatDatepickerControl<D>, S,
// datepicker dialog behaves consistently even if the user changed the defaults.
hasBackdrop: true,
disableClose: false,
backdropClass: ['cdk-overlay-dark-backdrop', this._backdropHarnessClass],
width: '',
height: '',
minWidth: '',
Expand Down Expand Up @@ -572,7 +576,7 @@ export abstract class MatDatepickerBase<C extends MatDatepickerControl<D>, S,
const overlayConfig = new OverlayConfig({
positionStrategy: this._setConnectedPositions(positionStrategy),
hasBackdrop: true,
backdropClass: 'mat-overlay-transparent-backdrop',
backdropClass: ['mat-overlay-transparent-backdrop', this._backdropHarnessClass],
direction: this._dir,
scrollStrategy: this._scrollStrategy(),
panelClass: 'mat-datepicker-popup',
Expand Down
4 changes: 4 additions & 0 deletions src/material/datepicker/datepicker-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,14 @@ export const MAT_DATEPICKER_VALIDATORS: any = {
{provide: MAT_INPUT_VALUE_ACCESSOR, useExisting: MatDatepickerInput},
],
host: {
'class': 'mat-datepicker-input',
'[attr.aria-haspopup]': '_datepicker ? "dialog" : null',
'[attr.aria-owns]': '(_datepicker?.opened && _datepicker.id) || null',
'[attr.min]': 'min ? _dateAdapter.toIso8601(min) : null',
'[attr.max]': 'max ? _dateAdapter.toIso8601(max) : null',
// Used by the test harness to tie this input to its calendar. We can't depend on
// `aria-owns` for this, because it's only defined while the calendar is open.
'[attr.data-mat-calendar]': '_datepicker ? _datepicker.id : null',
'[disabled]': 'disabled',
'(input)': '_onInput($event.target.value)',
'(change)': '_onChange()',
Expand Down
2 changes: 2 additions & 0 deletions src/material/datepicker/datepicker-toggle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ export class MatDatepickerToggleIcon {}
'[class.mat-datepicker-toggle-active]': 'datepicker && datepicker.opened',
'[class.mat-accent]': 'datepicker && datepicker.color === "accent"',
'[class.mat-warn]': 'datepicker && datepicker.color === "warn"',
// Used by the test harness to tie this toggle to its datepicker.
'[attr.data-mat-calendar]': 'datepicker ? datepicker.id : null',
'(focus)': '_button.focus()',
},
exportAs: 'matDatepickerToggle',
Expand Down
12 changes: 11 additions & 1 deletion src/material/datepicker/month-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ import {
ViewEncapsulation,
ViewChild,
OnDestroy,
SimpleChanges,
OnChanges,
} from '@angular/core';
import {DateAdapter, MAT_DATE_FORMATS, MatDateFormats} from '@angular/material/core';
import {Directionality} from '@angular/cdk/bidi';
Expand Down Expand Up @@ -65,7 +67,7 @@ const DAYS_PER_WEEK = 7;
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class MatMonthView<D> implements AfterContentInit, OnDestroy {
export class MatMonthView<D> implements AfterContentInit, OnChanges, OnDestroy {
private _rerenderSubscription = Subscription.EMPTY;

/**
Expand Down Expand Up @@ -199,6 +201,14 @@ export class MatMonthView<D> implements AfterContentInit, OnDestroy {
.subscribe(() => this._init());
}

ngOnChanges(changes: SimpleChanges) {
const comparisonChange = changes['comparisonStart'] || changes['comparisonEnd'];

if (comparisonChange && !comparisonChange.firstChange) {
this._setRanges(this.selected);
}
}

ngOnDestroy() {
this._rerenderSubscription.unsubscribe();
}
Expand Down
63 changes: 63 additions & 0 deletions src/material/datepicker/testing/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
load("//tools:defaults.bzl", "ng_test_library", "ng_web_test_suite", "ts_library")

package(default_visibility = ["//visibility:public"])

ts_library(
name = "testing",
srcs = glob(
["**/*.ts"],
exclude = ["**/*.spec.ts"],
),
module_name = "@angular/material/datepicker/testing",
deps = [
"//src/cdk/coercion",
"//src/cdk/testing",
],
)

filegroup(
name = "source-files",
srcs = glob(["**/*.ts"]),
)

ng_test_library(
name = "harness_tests_lib",
srcs = [
"calendar-harness-shared.spec.ts",
"date-range-input-harness-shared.spec.ts",
"datepicker-input-harness-shared.spec.ts",
"datepicker-toggle-harness-shared.spec.ts",
],
deps = [
":testing",
"//src/cdk/testing",
"//src/cdk/testing/testbed",
"//src/material/core",
"//src/material/datepicker",
"@npm//@angular/forms",
"@npm//@angular/platform-browser",
],
)

ng_test_library(
name = "unit_tests_lib",
srcs = glob(
["**/*.spec.ts"],
exclude = [
"date-range-input-harness-shared.spec.ts",
"datepicker-input-harness-shared.spec.ts",
"datepicker-toggle-harness-shared.spec.ts",
"calendar-harness-shared.spec.ts",
],
),
deps = [
":harness_tests_lib",
":testing",
"//src/material/datepicker",
],
)

ng_web_test_suite(
name = "unit_tests",
deps = [":unit_tests_lib"],
)
161 changes: 161 additions & 0 deletions src/material/datepicker/testing/calendar-cell-harness.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import {HarnessPredicate, ComponentHarness} from '@angular/cdk/testing';
import {CalendarCellHarnessFilters} from './datepicker-harness-filters';

/** Harness for interacting with a standard Material calendar cell in tests. */
export class MatCalendarCellHarness extends ComponentHarness {
static hostSelector = '.mat-calendar-body-cell';

/** Reference to the inner content element inside the cell. */
private _content = this.locatorFor('.mat-calendar-body-cell-content');

/**
* Gets a `HarnessPredicate` that can be used to search for a `MatCalendarCellHarness`
* that meets certain criteria.
* @param options Options for filtering which cell instances are considered a match.
* @return a `HarnessPredicate` configured with the given options.
*/
static with(options: CalendarCellHarnessFilters = {}): HarnessPredicate<MatCalendarCellHarness> {
return new HarnessPredicate(MatCalendarCellHarness, options)
.addOption('text', options.text, (harness, text) => {
return HarnessPredicate.stringMatches(harness.getText(), text);
})
.addOption('selected', options.selected, async (harness, selected) => {
return (await harness.isSelected()) === selected;
})
.addOption('active', options.active, async (harness, active) => {
return (await harness.isActive()) === active;
})
.addOption('disabled', options.disabled, async (harness, disabled) => {
return (await harness.isDisabled()) === disabled;
})
.addOption('today', options.today, async (harness, today) => {
return (await harness.isToday()) === today;
})
.addOption('inRange', options.inRange, async (harness, inRange) => {
return (await harness.isInRange()) === inRange;
})
.addOption('inComparisonRange', options.inComparisonRange,
async (harness, inComparisonRange) => {
return (await harness.isInComparisonRange()) === inComparisonRange;
})
.addOption('inPreviewRange', options.inPreviewRange, async (harness, inPreviewRange) => {
return (await harness.isInPreviewRange()) === inPreviewRange;
});
}

/** Gets the text of the calendar cell. */
async getText(): Promise<string> {
return (await this._content()).text();
}

/** Gets the aria-label of the calendar cell. */
async getAriaLabel(): Promise<string> {
// We're guaranteed for the `aria-label` to be defined
// since this is a private element that we control.
return (await this.host()).getAttribute('aria-label') as Promise<string>;
}

/** Whether the cell is selected. */
async isSelected(): Promise<boolean> {
const host = await this.host();
return (await host.getAttribute('aria-selected')) === 'true';
}

/** Whether the cell is disabled. */
async isDisabled(): Promise<boolean> {
return this._hasState('disabled');
}

/** Whether the cell is currently activated using keyboard navigation. */
async isActive(): Promise<boolean> {
return this._hasState('active');
}

/** Whether the cell represents today's date. */
async isToday(): Promise<boolean> {
return (await this._content()).hasClass('mat-calendar-body-today');
}

/** Selects the calendar cell. Won't do anything if the cell is disabled. */
async select(): Promise<void> {
return (await this.host()).click();
}

/** Hovers over the calendar cell. */
async hover(): Promise<void> {
return (await this.host()).hover();
}

/** Moves the mouse away from the calendar cell. */
async mouseAway(): Promise<void> {
return (await this.host()).mouseAway();
}

/** Focuses the calendar cell. */
async focus(): Promise<void> {
return (await this.host()).focus();
}

/** Removes focus from the calendar cell. */
async blur(): Promise<void> {
return (await this.host()).blur();
}

/** Whether the cell is the start of the main range. */
async isRangeStart(): Promise<boolean> {
return this._hasState('range-start');
}

/** Whether the cell is the end of the main range. */
async isRangeEnd(): Promise<boolean> {
return this._hasState('range-end');
}

/** Whether the cell is part of the main range. */
async isInRange(): Promise<boolean> {
return this._hasState('in-range');
}

/** Whether the cell is the start of the comparison range. */
async isComparisonRangeStart(): Promise<boolean> {
return this._hasState('comparison-start');
}

/** Whether the cell is the end of the comparison range. */
async isComparisonRangeEnd(): Promise<boolean> {
return this._hasState('comparison-end');
}

/** Whether the cell is inside of the comparison range. */
async isInComparisonRange(): Promise<boolean> {
return this._hasState('in-comparison-range');
}

/** Whether the cell is the start of the preview range. */
async isPreviewRangeStart(): Promise<boolean> {
return this._hasState('preview-start');
}

/** Whether the cell is the end of the preview range. */
async isPreviewRangeEnd(): Promise<boolean> {
return this._hasState('preview-end');
}

/** Whether the cell is inside of the preview range. */
async isInPreviewRange(): Promise<boolean> {
return this._hasState('in-preview');
}

/** Returns whether the cell has a particular CSS class-based state. */
private async _hasState(name: string): Promise<boolean> {
return (await this.host()).hasClass(`mat-calendar-body-${name}`);
}
}
Loading