diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index b843f43451f8..61d0f67fba84 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -94,6 +94,8 @@ /src/material-experimental/mdc-radio/** @mmalerba /src/material-experimental/mdc-slide-toggle/** @crisbeto /src/material-experimental/mdc-tabs/** @crisbeto +/src/material-experimental/mdc-theming/** @mmalerba +/src/material-experimental/mdc-typography/** @mmalerba /src/material-experimental/popover-edit/** @kseamon @andrewseguin # CDK experimental package diff --git a/src/cdk-experimental/testing/BUILD.bazel b/src/cdk-experimental/testing/BUILD.bazel index 7727dd4d60d7..345c147669a6 100644 --- a/src/cdk-experimental/testing/BUILD.bazel +++ b/src/cdk-experimental/testing/BUILD.bazel @@ -9,15 +9,9 @@ ng_module( ["**/*.ts"], exclude = [ "**/*.spec.ts", - "tests/**", ], ), module_name = "@angular/cdk-experimental/testing", - deps = [ - "//src/cdk/testing", - "@npm//@angular/core", - "@npm//protractor", - ], ) ng_web_test_suite( diff --git a/src/cdk-experimental/testing/protractor/BUILD b/src/cdk-experimental/testing/protractor/BUILD new file mode 100644 index 000000000000..0377b85756a0 --- /dev/null +++ b/src/cdk-experimental/testing/protractor/BUILD @@ -0,0 +1,16 @@ +package(default_visibility = ["//visibility:public"]) + +load("//tools:defaults.bzl", "ts_library") + +ts_library( + name = "protractor", + srcs = glob( + ["**/*.ts"], + exclude = ["**/*.spec.ts"], + ), + module_name = "@angular/cdk-experimental/testing/protractor", + deps = [ + "//src/cdk-experimental/testing", + "@npm//protractor", + ], +) diff --git a/src/cdk-experimental/testing/protractor/index.ts b/src/cdk-experimental/testing/protractor/index.ts index 7b4ab8f52b30..676ca90f1ffa 100644 --- a/src/cdk-experimental/testing/protractor/index.ts +++ b/src/cdk-experimental/testing/protractor/index.ts @@ -6,5 +6,4 @@ * found in the LICENSE file at https://angular.io/license */ -export * from './protractor-element'; -export * from './protractor-harness-environment'; +export * from './public-api'; diff --git a/src/cdk-experimental/testing/protractor/protractor-element.ts b/src/cdk-experimental/testing/protractor/protractor-element.ts index 1a89bde62bcb..92aed550030b 100644 --- a/src/cdk-experimental/testing/protractor/protractor-element.ts +++ b/src/cdk-experimental/testing/protractor/protractor-element.ts @@ -50,4 +50,9 @@ export class ProtractorElement implements TestElement { async getAttribute(name: string): Promise { return this.element.getAttribute(name); } + + async hasClass(name: string): Promise { + const classes = (await this.getAttribute('class')) || ''; + return new Set(classes.split(/\s+/).filter(c => c)).has(name); + } } diff --git a/src/cdk-experimental/testing/protractor/public-api.ts b/src/cdk-experimental/testing/protractor/public-api.ts new file mode 100644 index 000000000000..7b4ab8f52b30 --- /dev/null +++ b/src/cdk-experimental/testing/protractor/public-api.ts @@ -0,0 +1,10 @@ +/** + * @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 + */ + +export * from './protractor-element'; +export * from './protractor-harness-environment'; diff --git a/src/cdk-experimental/testing/public-api.ts b/src/cdk-experimental/testing/public-api.ts index d90b5ede9b41..5c8698a549e6 100644 --- a/src/cdk-experimental/testing/public-api.ts +++ b/src/cdk-experimental/testing/public-api.ts @@ -8,6 +8,4 @@ export * from './component-harness'; export * from './harness-environment'; -export * from './protractor'; export * from './test-element'; -export * from './testbed'; diff --git a/src/cdk-experimental/testing/test-element.ts b/src/cdk-experimental/testing/test-element.ts index 1c6ae8de55cb..9c9abf780df0 100644 --- a/src/cdk-experimental/testing/test-element.ts +++ b/src/cdk-experimental/testing/test-element.ts @@ -43,4 +43,7 @@ export interface TestElement { * falls back to reading the property. */ getAttribute(name: string): Promise; + + /** Checks whether the element has the given class. */ + hasClass(name: string): Promise; } diff --git a/src/cdk-experimental/testing/testbed/BUILD b/src/cdk-experimental/testing/testbed/BUILD new file mode 100644 index 000000000000..56d93eadb969 --- /dev/null +++ b/src/cdk-experimental/testing/testbed/BUILD @@ -0,0 +1,17 @@ +package(default_visibility = ["//visibility:public"]) + +load("//tools:defaults.bzl", "ts_library") + +ts_library( + name = "testbed", + srcs = glob( + ["**/*.ts"], + exclude = ["**/*.spec.ts"], + ), + module_name = "@angular/cdk-experimental/testing/testbed", + deps = [ + "//src/cdk-experimental/testing", + "//src/cdk/testing", + "@npm//@angular/core", + ], +) diff --git a/src/cdk-experimental/testing/testbed/index.ts b/src/cdk-experimental/testing/testbed/index.ts index 0422b3097bf3..676ca90f1ffa 100644 --- a/src/cdk-experimental/testing/testbed/index.ts +++ b/src/cdk-experimental/testing/testbed/index.ts @@ -6,5 +6,4 @@ * found in the LICENSE file at https://angular.io/license */ -export * from './testbed-harness-environment'; -export * from './unit-test-element'; +export * from './public-api'; diff --git a/src/cdk-experimental/testing/testbed/public-api.ts b/src/cdk-experimental/testing/testbed/public-api.ts new file mode 100644 index 000000000000..0422b3097bf3 --- /dev/null +++ b/src/cdk-experimental/testing/testbed/public-api.ts @@ -0,0 +1,10 @@ +/** + * @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 + */ + +export * from './testbed-harness-environment'; +export * from './unit-test-element'; diff --git a/src/cdk-experimental/testing/testbed/unit-test-element.ts b/src/cdk-experimental/testing/testbed/unit-test-element.ts index b778ef307d0e..2ccc56d0f5be 100644 --- a/src/cdk-experimental/testing/testbed/unit-test-element.ts +++ b/src/cdk-experimental/testing/testbed/unit-test-element.ts @@ -86,7 +86,7 @@ export class UnitTestElement implements TestElement { async text(): Promise { await this._stabilize(); - return this.element.textContent || ''; + return (this.element.textContent || '').trim(); } async getAttribute(name: string): Promise { @@ -100,4 +100,9 @@ export class UnitTestElement implements TestElement { } return value; } + + async hasClass(name: string): Promise { + await this._stabilize(); + return this.element.classList.contains(name); + } } diff --git a/src/cdk-experimental/testing/tests/BUILD.bazel b/src/cdk-experimental/testing/tests/BUILD.bazel index 19556f7d3d1d..bba409024227 100644 --- a/src/cdk-experimental/testing/tests/BUILD.bazel +++ b/src/cdk-experimental/testing/tests/BUILD.bazel @@ -36,6 +36,7 @@ ng_test_library( ":test_components", ":test_harnesses", "//src/cdk-experimental/testing", + "//src/cdk-experimental/testing/testbed", ], ) @@ -45,5 +46,6 @@ ng_e2e_test_library( deps = [ ":test_harnesses", "//src/cdk-experimental/testing", + "//src/cdk-experimental/testing/protractor", ], ) diff --git a/src/cdk-experimental/testing/tests/protractor.e2e.spec.ts b/src/cdk-experimental/testing/tests/protractor.e2e.spec.ts index 7909f50f3c61..1e882c93e875 100644 --- a/src/cdk-experimental/testing/tests/protractor.e2e.spec.ts +++ b/src/cdk-experimental/testing/tests/protractor.e2e.spec.ts @@ -1,6 +1,6 @@ +import {HarnessLoader} from '@angular/cdk-experimental/testing'; +import {ProtractorHarnessEnvironment} from '@angular/cdk-experimental/testing/protractor'; import {browser} from 'protractor'; -import {HarnessLoader} from '../component-harness'; -import {ProtractorHarnessEnvironment} from '../protractor'; import {MainComponentHarness} from './harnesses/main-component-harness'; import {SubComponentHarness} from './harnesses/sub-component-harness'; diff --git a/src/cdk-experimental/testing/tests/testbed.spec.ts b/src/cdk-experimental/testing/tests/testbed.spec.ts index dfe7d44ad745..9ef5f70a7e6b 100644 --- a/src/cdk-experimental/testing/tests/testbed.spec.ts +++ b/src/cdk-experimental/testing/tests/testbed.spec.ts @@ -1,6 +1,6 @@ +import {HarnessLoader} from '@angular/cdk-experimental/testing'; +import {TestbedHarnessEnvironment} from '@angular/cdk-experimental/testing/testbed'; import {ComponentFixture, TestBed} from '@angular/core/testing'; -import {HarnessLoader} from '../component-harness'; -import {TestbedHarnessEnvironment} from '../testbed/index'; import {MainComponentHarness} from './harnesses/main-component-harness'; import {SubComponentHarness} from './harnesses/sub-component-harness'; import {TestComponentsModule} from './test-components-module'; diff --git a/src/material-experimental/mdc-checkbox/BUILD.bazel b/src/material-experimental/mdc-checkbox/BUILD.bazel index 679220dbdaeb..89c4670e0390 100644 --- a/src/material-experimental/mdc-checkbox/BUILD.bazel +++ b/src/material-experimental/mdc-checkbox/BUILD.bazel @@ -2,13 +2,16 @@ package(default_visibility = ["//visibility:public"]) load("@io_bazel_rules_sass//:defs.bzl", "sass_binary", "sass_library") load("@npm_angular_bazel//:index.bzl", "protractor_web_test_suite") -load("//tools:defaults.bzl", "ng_e2e_test_library", "ng_module", "ng_test_library", "ng_web_test_suite") +load("//tools:defaults.bzl", "ng_e2e_test_library", "ng_module", "ng_test_library", "ng_web_test_suite", "ts_library") ng_module( name = "mdc-checkbox", srcs = glob( ["**/*.ts"], - exclude = ["**/*.spec.ts"], + exclude = [ + "**/*.spec.ts", + "harness/**", + ], ), assets = [":checkbox_scss"] + glob(["**/*.html"]), module_name = "@angular/material-experimental/mdc-checkbox", @@ -25,6 +28,18 @@ ng_module( ], ) +ts_library( + name = "harness", + srcs = glob( + ["harness/**/*.ts"], + exclude = ["**/*.spec.ts"], + ), + deps = [ + "//src/cdk-experimental/testing", + "//src/cdk/coercion", + ], +) + sass_library( name = "mdc_checkbox_scss_lib", srcs = glob(["**/_*.scss"]), @@ -55,8 +70,12 @@ ng_test_library( exclude = ["**/*.e2e.spec.ts"], ), deps = [ + ":harness", ":mdc-checkbox", + "//src/cdk-experimental/testing", + "//src/cdk-experimental/testing/testbed", "//src/cdk/testing", + "//src/material/checkbox", "@npm//@angular/forms", "@npm//@angular/platform-browser", ], diff --git a/src/material-experimental/mdc-checkbox/harness/checkbox-harness-filters.ts b/src/material-experimental/mdc-checkbox/harness/checkbox-harness-filters.ts new file mode 100644 index 000000000000..a82a027b5cbb --- /dev/null +++ b/src/material-experimental/mdc-checkbox/harness/checkbox-harness-filters.ts @@ -0,0 +1,11 @@ +/** + * @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 + */ + +export type CheckboxHarnessFilters = { + label?: string | RegExp +}; diff --git a/src/material-experimental/mdc-checkbox/harness/checkbox-harness.spec.ts b/src/material-experimental/mdc-checkbox/harness/checkbox-harness.spec.ts new file mode 100644 index 000000000000..1b0c1809e064 --- /dev/null +++ b/src/material-experimental/mdc-checkbox/harness/checkbox-harness.spec.ts @@ -0,0 +1,202 @@ +import {HarnessLoader} from '@angular/cdk-experimental/testing'; +import {TestbedHarnessEnvironment} from '@angular/cdk-experimental/testing/testbed'; +import {Component} from '@angular/core'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {FormControl, ReactiveFormsModule} from '@angular/forms'; +import {MatCheckboxModule} from '@angular/material/checkbox'; +import {MatCheckboxModule as MatMdcCheckboxModule} from '../index'; +import {MatCheckboxHarness} from './checkbox-harness'; +import {MatCheckboxHarness as MatMdcCheckboxHarness} from './mdc-checkbox-harness'; + +let fixture: ComponentFixture; +let loader: HarnessLoader; +let checkboxHarness: typeof MatCheckboxHarness; + +describe('MatCheckboxHarness', () => { + describe('non-MDC-based', () => { + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [MatCheckboxModule, ReactiveFormsModule], + declarations: [CheckboxHarnessTest], + }).compileComponents(); + + fixture = TestBed.createComponent(CheckboxHarnessTest); + fixture.detectChanges(); + loader = TestbedHarnessEnvironment.loader(fixture); + checkboxHarness = MatCheckboxHarness; + }); + + runTests(); + }); + + describe('MDC-based', () => { + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [MatMdcCheckboxModule, ReactiveFormsModule], + declarations: [CheckboxHarnessTest], + }).compileComponents(); + + fixture = TestBed.createComponent(CheckboxHarnessTest); + fixture.detectChanges(); + loader = TestbedHarnessEnvironment.loader(fixture); + // Public APIs are the same as MatCheckboxHarness, but cast is necessary because of different + // private fields. + checkboxHarness = MatMdcCheckboxHarness as any; + }); + + runTests(); + }); +}); + +/** Shared tests to run on both the original and MDC-based checkboxes. */ +function runTests() { + it('should load all checkbox harnesses', async () => { + const checkboxes = await loader.getAllHarnesses(checkboxHarness); + expect(checkboxes.length).toBe(2); + }); + + it('should load checkbox with exact label', async () => { + const checkboxes = await loader.getAllHarnesses(checkboxHarness.with({label: 'First'})); + expect(checkboxes.length).toBe(1); + expect(await checkboxes[0].getLabelText()).toBe('First'); + }); + + it('should load checkbox with regex label match', async () => { + const checkboxes = await loader.getAllHarnesses(checkboxHarness.with({label: /^s/i})); + expect(checkboxes.length).toBe(1); + expect(await checkboxes[0].getLabelText()).toBe('Second'); + }); + + it('should get checked state', async () => { + const [checkedCheckbox, uncheckedCheckbox] = await loader.getAllHarnesses(checkboxHarness); + expect(await checkedCheckbox.isChecked()).toBe(true); + expect(await uncheckedCheckbox.isChecked()).toBe(false); + }); + + it('should get indeterminate state', async () => { + const [checkedCheckbox, indeterminateCheckbox] = await loader.getAllHarnesses(checkboxHarness); + expect(await checkedCheckbox.isIndeterminate()).toBe(false); + expect(await indeterminateCheckbox.isIndeterminate()).toBe(true); + }); + + it('should get disabled state', async () => { + const [enabledCheckbox, disabledCheckbox] = await loader.getAllHarnesses(checkboxHarness); + expect(await enabledCheckbox.isDisabled()).toBe(false); + expect(await disabledCheckbox.isDisabled()).toBe(true); + }); + + it('should get required state', async () => { + const [requiredCheckbox, optionalCheckbox] = await loader.getAllHarnesses(checkboxHarness); + expect(await requiredCheckbox.isRequired()).toBe(true); + expect(await optionalCheckbox.isRequired()).toBe(false); + }); + + it('should get valid state', async () => { + const [requiredCheckbox, optionalCheckbox] = await loader.getAllHarnesses(checkboxHarness); + expect(await optionalCheckbox.isValid()).toBe(true); + expect(await requiredCheckbox.isValid()).toBe(true); + await requiredCheckbox.uncheck(); + expect(await requiredCheckbox.isValid()).toBe(false); + }); + + it('should get name', async () => { + const checkbox = await loader.getHarness(checkboxHarness.with({label: 'First'})); + expect(await checkbox.getName()).toBe('first-name'); + }); + + it('should get value', async () => { + const checkbox = await loader.getHarness(checkboxHarness.with({label: 'First'})); + expect(await checkbox.getValue()).toBe('first-value'); + }); + + it('should get aria-label', async () => { + const checkbox = await loader.getHarness(checkboxHarness.with({label: 'First'})); + expect(await checkbox.getAriaLabel()).toBe('First checkbox'); + }); + + it('should get aria-labelledby', async () => { + const checkbox = await loader.getHarness(checkboxHarness.with({label: 'Second'})); + expect(await checkbox.getAriaLabelledby()).toBe('second-label'); + }); + + it('should get label text', async () => { + const [firstCheckbox, secondCheckbox] = await loader.getAllHarnesses(checkboxHarness); + expect(await firstCheckbox.getLabelText()).toBe('First'); + expect(await secondCheckbox.getLabelText()).toBe('Second'); + }); + + it('should focus checkbox', async () => { + const checkbox = await loader.getHarness(checkboxHarness.with({label: 'First'})); + expect(getActiveElementTagName()).not.toBe('input'); + await checkbox.foucs(); + expect(getActiveElementTagName()).toBe('input'); + }); + + it('should blur checkbox', async () => { + const checkbox = await loader.getHarness(checkboxHarness.with({label: 'First'})); + await checkbox.foucs(); + expect(getActiveElementTagName()).toBe('input'); + await checkbox.blur(); + expect(getActiveElementTagName()).not.toBe('input'); + }); + + it('should toggle checkbox', async () => { + fixture.componentInstance.disabled = false; + const [checkedCheckbox, uncheckedCheckbox] = await loader.getAllHarnesses(checkboxHarness); + await checkedCheckbox.toggle(); + await uncheckedCheckbox.toggle(); + expect(await checkedCheckbox.isChecked()).toBe(false); + expect(await uncheckedCheckbox.isChecked()).toBe(true); + }); + + it('should check checkbox', async () => { + fixture.componentInstance.disabled = false; + const [checkedCheckbox, uncheckedCheckbox] = await loader.getAllHarnesses(checkboxHarness); + await checkedCheckbox.check(); + await uncheckedCheckbox.check(); + expect(await checkedCheckbox.isChecked()).toBe(true); + expect(await uncheckedCheckbox.isChecked()).toBe(true); + }); + + it('should uncheck checkbox', async () => { + fixture.componentInstance.disabled = false; + const [checkedCheckbox, uncheckedCheckbox] = await loader.getAllHarnesses(checkboxHarness); + await checkedCheckbox.uncheck(); + await uncheckedCheckbox.uncheck(); + expect(await checkedCheckbox.isChecked()).toBe(false); + expect(await uncheckedCheckbox.isChecked()).toBe(false); + }); + + it('should not toggle disabled checkbox', async () => { + const disabledCheckbox = await loader.getHarness(checkboxHarness.with({label: 'Second'})); + expect(await disabledCheckbox.isChecked()).toBe(false); + await disabledCheckbox.toggle(); + expect(await disabledCheckbox.isChecked()).toBe(false); + }); +} + +function getActiveElementTagName() { + return document.activeElement ? document.activeElement.tagName.toLowerCase() : ''; +} + +@Component({ + template: ` + + First + + + Second + + Second checkbox + ` +}) +class CheckboxHarnessTest { + ctrl = new FormControl(true); + disabled = true; +} + diff --git a/src/material-experimental/mdc-checkbox/harness/checkbox-harness.ts b/src/material-experimental/mdc-checkbox/harness/checkbox-harness.ts new file mode 100644 index 000000000000..6d52da66e03b --- /dev/null +++ b/src/material-experimental/mdc-checkbox/harness/checkbox-harness.ts @@ -0,0 +1,142 @@ +/** + * @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 {ComponentHarness, HarnessPredicate} from '@angular/cdk-experimental/testing'; +import {coerceBooleanProperty} from '@angular/cdk/coercion'; +import {CheckboxHarnessFilters} from './checkbox-harness-filters'; + +/** + * Harness for interacting with a standard mat-checkbox in tests. + * @dynamic + */ +export class MatCheckboxHarness extends ComponentHarness { + static hostSelector = 'mat-checkbox'; + + /** + * Gets a `HarnessPredicate` that can be used to search for a checkbox with specific attributes. + * @param options Options for narrowing the search: + * - `label` finds a checkbox with specific label text. + * @return a `HarnessPredicate` configured with the given options. + */ + static with(options: CheckboxHarnessFilters = {}): HarnessPredicate { + return new HarnessPredicate(MatCheckboxHarness) + .addOption('label', options.label, + (harness, label) => HarnessPredicate.stringMatches(harness.getLabelText(), label)); + } + + private _label = this.locatorFor('.mat-checkbox-label'); + private _input = this.locatorFor('input'); + private _inputContainer = this.locatorFor('.mat-checkbox-inner-container'); + + /** Gets a boolean promise indicating if the checkbox is checked. */ + async isChecked(): Promise { + const checked = (await this._input()).getAttribute('checked'); + return coerceBooleanProperty(await checked); + } + + /** Gets a boolean promise indicating if the checkbox is in an indeterminate state. */ + async isIndeterminate(): Promise { + const indeterminate = (await this._input()).getAttribute('indeterminate'); + return coerceBooleanProperty(await indeterminate); + } + + /** Gets a boolean promise indicating if the checkbox is disabled. */ + async isDisabled(): Promise { + const disabled = (await this._input()).getAttribute('disabled'); + return coerceBooleanProperty(await disabled); + } + + /** Gets a boolean promise indicating if the checkbox is required. */ + async isRequired(): Promise { + const required = (await this._input()).getAttribute('required'); + return coerceBooleanProperty(await required); + } + + /** Gets a boolean promise indicating if the checkbox is valid. */ + async isValid(): Promise { + const invalid = (await this.host()).hasClass('ng-invalid'); + return !(await invalid); + } + + /** Gets a promise for the checkbox's name. */ + async getName(): Promise { + return (await this._input()).getAttribute('name'); + } + + /** Gets a promise for the checkbox's value. */ + async getValue(): Promise { + return (await this._input()).getAttribute('value'); + } + + /** Gets a promise for the checkbox's aria-label. */ + async getAriaLabel(): Promise { + return (await this._input()).getAttribute('aria-label'); + } + + /** Gets a promise for the checkbox's aria-labelledby. */ + async getAriaLabelledby(): Promise { + return (await this._input()).getAttribute('aria-labelledby'); + } + + /** Gets a promise for the checkbox's label text. */ + async getLabelText(): Promise { + return (await this._label()).text(); + } + + /** Focuses the checkbox and returns a void promise that indicates when the action is complete. */ + async foucs(): Promise { + return (await this._input()).focus(); + } + + /** Blurs the checkbox and returns a void promise that indicates when the action is complete. */ + async blur(): Promise { + return (await this._input()).blur(); + } + + /** + * Toggle the checked state of the checkbox and returns a void promise that indicates when the + * action is complete. + * + * Note: This attempts to toggle the checkbox as a user would, by clicking it. Therefore if you + * are using `MAT_CHECKBOX_CLICK_ACTION` to change the behavior on click, calling this method + * might not have the expected result. + */ + async toggle(): Promise { + return (await this._inputContainer()).click(); + } + + /** + * Puts the checkbox in a checked state by toggling it if it is currently unchecked, or doing + * nothing if it is already checked. Returns a void promise that indicates when the action is + * complete. + * + * Note: This attempts to check the checkbox as a user would, by clicking it. Therefore if you + * are using `MAT_CHECKBOX_CLICK_ACTION` to change the behavior on click, calling this method + * might not have the expected result. + */ + async check(): Promise { + if (!(await this.isChecked())) { + await this.toggle(); + } + } + + /** + * Puts the checkbox in an unchecked state by toggling it if it is currently checked, or doing + * nothing if it is already unchecked. Returns a void promise that indicates when the action is + * complete. + * + * Note: This attempts to uncheck the checkbox as a user would, by clicking it. Therefore if you + * are using `MAT_CHECKBOX_CLICK_ACTION` to change the behavior on click, calling this method + * might not have the expected result. + */ + async uncheck(): Promise { + if (await this.isChecked()) { + await this.toggle(); + } + } +} diff --git a/src/material-experimental/mdc-checkbox/harness/mdc-checkbox-harness.ts b/src/material-experimental/mdc-checkbox/harness/mdc-checkbox-harness.ts new file mode 100644 index 000000000000..f1f4b9c2c075 --- /dev/null +++ b/src/material-experimental/mdc-checkbox/harness/mdc-checkbox-harness.ts @@ -0,0 +1,143 @@ +/** + * @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 {ComponentHarness, HarnessPredicate} from '@angular/cdk-experimental/testing'; +import {coerceBooleanProperty} from '@angular/cdk/coercion'; +import {CheckboxHarnessFilters} from './checkbox-harness-filters'; + +/** + * Harness for interacting with a MDC-based mat-checkbox in tests. + * @dynamic + */ +export class MatCheckboxHarness extends ComponentHarness { + static hostSelector = 'mat-checkbox'; + + /** + * Gets a `HarnessPredicate` that can be used to search for a checkbox with specific attributes. + * @param options Options for narrowing the search: + * - `label` finds a checkbox with specific label text. + * @return a `HarnessPredicate` configured with the given options. + */ + static with(options: CheckboxHarnessFilters = {}): HarnessPredicate { + return new HarnessPredicate(MatCheckboxHarness) + .addOption('label', options.label, + (harness, label) => HarnessPredicate.stringMatches(harness.getLabelText(), label)); + } + + private _label = this.locatorFor('label'); + private _input = this.locatorFor('input'); + private _inputContainer = this.locatorFor('.mdc-checkbox'); + + /** Gets a boolean promise indicating if the checkbox is checked. */ + async isChecked(): Promise { + const checked = (await this._input()).getAttribute('checked'); + return coerceBooleanProperty(await checked); + } + + /** Gets a boolean promise indicating if the checkbox is in an indeterminate state. */ + async isIndeterminate(): Promise { + const indeterminate = (await this._input()).getAttribute('indeterminate'); + return coerceBooleanProperty(await indeterminate); + } + + /** Gets a boolean promise indicating if the checkbox is disabled. */ + async isDisabled(): Promise { + const disabled = (await this._input()).getAttribute('disabled'); + return coerceBooleanProperty(await disabled); + } + + /** Gets a boolean promise indicating if the checkbox is required. */ + async isRequired(): Promise { + const required = (await this._input()).getAttribute('required'); + return coerceBooleanProperty(await required); + } + + /** Gets a boolean promise indicating if the checkbox is valid. */ + async isValid(): Promise { + const invalid = (await this.host()).hasClass('ng-invalid'); + return !(await invalid); + } + + /** Gets a promise for the checkbox's name. */ + async getName(): Promise { + return (await this._input()).getAttribute('name'); + } + + /** Gets a promise for the checkbox's value. */ + async getValue(): Promise { + return (await this._input()).getAttribute('value'); + } + + /** Gets a promise for the checkbox's aria-label. */ + async getAriaLabel(): Promise { + return (await this._input()).getAttribute('aria-label'); + } + + /** Gets a promise for the checkbox's aria-labelledby. */ + async getAriaLabelledby(): Promise { + return (await this._input()).getAttribute('aria-labelledby'); + } + + /** Gets a promise for the checkbox's label text. */ + async getLabelText(): Promise { + return (await this._label()).text(); + } + + /** Focuses the checkbox and returns a void promise that indicates when the action is complete. */ + async foucs(): Promise { + return (await this._input()).focus(); + } + + /** Blurs the checkbox and returns a void promise that indicates when the action is complete. */ + async blur(): Promise { + return (await this._input()).blur(); + } + + /** + * Toggle the checked state of the checkbox and returns a void promise that indicates when the + * action is complete. + * + * Note: This attempts to toggle the checkbox as a user would, by clicking it. Therefore if you + * are using `MAT_CHECKBOX_CLICK_ACTION` to change the behavior on click, calling this method + * might not have the expected result. + */ + async toggle(): Promise { + const elToClick = await this.isDisabled() ? this._inputContainer() : this._input(); + return (await elToClick).click(); + } + + /** + * Puts the checkbox in a checked state by toggling it if it is currently unchecked, or doing + * nothing if it is already checked. Returns a void promise that indicates when the action is + * complete. + * + * Note: This attempts to check the checkbox as a user would, by clicking it. Therefore if you + * are using `MAT_CHECKBOX_CLICK_ACTION` to change the behavior on click, calling this method + * might not have the expected result. + */ + async check(): Promise { + if (!(await this.isChecked())) { + await this.toggle(); + } + } + + /** + * Puts the checkbox in an unchecked state by toggling it if it is currently checked, or doing + * nothing if it is already unchecked. Returns a void promise that indicates when the action is + * complete. + * + * Note: This attempts to uncheck the checkbox as a user would, by clicking it. Therefore if you + * are using `MAT_CHECKBOX_CLICK_ACTION` to change the behavior on click, calling this method + * might not have the expected result. + */ + async uncheck(): Promise { + if (await this.isChecked()) { + await this.toggle(); + } + } +} diff --git a/src/material-experimental/mdc-theming/BUILD.bazel b/src/material-experimental/mdc-theming/BUILD.bazel new file mode 100644 index 000000000000..77601077fb8e --- /dev/null +++ b/src/material-experimental/mdc-theming/BUILD.bazel @@ -0,0 +1,32 @@ +package(default_visibility = ["//visibility:public"]) + +load("@io_bazel_rules_sass//:defs.bzl", "sass_binary", "sass_library") + +sass_library( + name = "all_themes", + srcs = [ + "_all-theme.scss", + ], + deps = [ + "//src/material-experimental/mdc-button:mdc_button_scss_lib", + "//src/material-experimental/mdc-card:mdc_card_scss_lib", + "//src/material-experimental/mdc-checkbox:mdc_checkbox_scss_lib", + "//src/material-experimental/mdc-chips:mdc_chips_scss_lib", + "//src/material-experimental/mdc-menu:mdc_menu_scss_lib", + "//src/material-experimental/mdc-radio:mdc_radio_scss_lib", + "//src/material-experimental/mdc-slide-toggle:mdc_slide_toggle_scss_lib", + "//src/material-experimental/mdc-tabs:mdc_tabs_scss_lib", + ], +) + +sass_binary( + name = "indigo_pink_prebuilt", + src = "prebuilt/indigo-pink.scss", + include_paths = [ + "external/npm/node_modules", + ], + deps = [ + ":all_themes", + "//src/material-experimental/mdc-typography:all_typography", + ], +) diff --git a/src/material-experimental/mdc-theming/_all-theme.scss b/src/material-experimental/mdc-theming/_all-theme.scss new file mode 100644 index 000000000000..e5e3d99caf67 --- /dev/null +++ b/src/material-experimental/mdc-theming/_all-theme.scss @@ -0,0 +1,21 @@ +@import '../mdc-button/mdc-button'; +@import '../mdc-card/mdc-card'; +@import '../mdc-checkbox/mdc-checkbox'; +@import '../mdc-chips/mdc-chips'; +@import '../mdc-menu/mdc-menu'; +@import '../mdc-radio/mdc-radio'; +@import '../mdc-slide-toggle/mdc-slide-toggle'; +@import '../mdc-tabs/mdc-tabs'; + +@mixin angular-material-theme-mdc($theme) { + @include mat-button-theme-mdc($theme); + @include mat-fab-theme-mdc($theme); + @include mat-icon-button-theme-mdc($theme); + @include mat-card-theme-mdc($theme); + @include mat-checkbox-theme-mdc($theme); + @include mat-chips-theme-mdc($theme); + @include mat-menu-theme-mdc($theme); + @include mat-radio-theme-mdc($theme); + @include mat-slide-toggle-theme-mdc($theme); + @include mat-tabs-theme-mdc($theme); +} diff --git a/src/material-experimental/mdc-theming/prebuilt/indigo-pink.scss b/src/material-experimental/mdc-theming/prebuilt/indigo-pink.scss new file mode 100644 index 000000000000..d87658dfa726 --- /dev/null +++ b/src/material-experimental/mdc-theming/prebuilt/indigo-pink.scss @@ -0,0 +1,12 @@ +@import '../all-theme'; +@import '../../mdc-typography/all-typography'; + +// Define a theme. +$primary: mat-palette($mat-indigo); +$accent: mat-palette($mat-pink, A200, A100, A400); + +$theme: mat-light-theme($primary, $accent); + +// Include all theme styles for the components. +@include angular-material-theme-mdc($theme); +@include angular-material-typography-mdc(); diff --git a/src/material-experimental/mdc-typography/BUILD.bazel b/src/material-experimental/mdc-typography/BUILD.bazel new file mode 100644 index 000000000000..9bd73694e688 --- /dev/null +++ b/src/material-experimental/mdc-typography/BUILD.bazel @@ -0,0 +1,20 @@ +package(default_visibility = ["//visibility:public"]) + +load("@io_bazel_rules_sass//:defs.bzl", "sass_library") + +sass_library( + name = "all_typography", + srcs = [ + "_all-typography.scss", + ], + deps = [ + "//src/material-experimental/mdc-button:mdc_button_scss_lib", + "//src/material-experimental/mdc-card:mdc_card_scss_lib", + "//src/material-experimental/mdc-checkbox:mdc_checkbox_scss_lib", + "//src/material-experimental/mdc-chips:mdc_chips_scss_lib", + "//src/material-experimental/mdc-menu:mdc_menu_scss_lib", + "//src/material-experimental/mdc-radio:mdc_radio_scss_lib", + "//src/material-experimental/mdc-slide-toggle:mdc_slide_toggle_scss_lib", + "//src/material-experimental/mdc-tabs:mdc_tabs_scss_lib", + ], +) diff --git a/src/material-experimental/mdc-typography/_all-typography.scss b/src/material-experimental/mdc-typography/_all-typography.scss new file mode 100644 index 000000000000..e825c3bf6cbd --- /dev/null +++ b/src/material-experimental/mdc-typography/_all-typography.scss @@ -0,0 +1,21 @@ +@import '../mdc-button/mdc-button'; +@import '../mdc-card/mdc-card'; +@import '../mdc-checkbox/mdc-checkbox'; +@import '../mdc-chips/mdc-chips'; +@import '../mdc-menu/mdc-menu'; +@import '../mdc-radio/mdc-radio'; +@import '../mdc-slide-toggle/mdc-slide-toggle'; +@import '../mdc-tabs/mdc-tabs'; + +@mixin angular-material-typography-mdc($config: null) { + @include mat-button-typography-mdc($config); + @include mat-fab-typography-mdc($config); + @include mat-icon-button-typography-mdc($config); + @include mat-card-typography-mdc($config); + @include mat-checkbox-typography-mdc($config); + @include mat-chips-typography-mdc($config); + @include mat-menu-typography-mdc($config); + @include mat-radio-typography-mdc($config); + @include mat-slide-toggle-typography-mdc($config); + @include mat-tabs-typography-mdc($config); +} diff --git a/test/karma-system-config.js b/test/karma-system-config.js index 3239888d0545..4477e597b873 100644 --- a/test/karma-system-config.js +++ b/test/karma-system-config.js @@ -93,6 +93,8 @@ System.config({ '@angular/cdk-experimental/popover-edit': 'dist/packages/cdk-experimental/popover-edit/index.js', '@angular/cdk-experimental/scrolling': 'dist/packages/cdk-experimental/scrolling/index.js', '@angular/cdk-experimental/testing': 'dist/packages/cdk-experimental/testing/index.js', + '@angular/cdk-experimental/testing/testbed': 'dist/packages/cdk-experimental/testing/testbed/index.js', + '@angular/cdk-experimental/testing/protractor': 'dist/packages/cdk-experimental/testing/protractor/index.js', '@angular/material/autocomplete': 'dist/packages/material/autocomplete/index.js', '@angular/material/badge': 'dist/packages/material/badge/index.js', diff --git a/tools/defaults.bzl b/tools/defaults.bzl index df961b7108ff..cc5dad27d11d 100644 --- a/tools/defaults.bzl +++ b/tools/defaults.bzl @@ -127,7 +127,10 @@ def ng_web_test_suite(deps = [], static_css = [], bootstrap = [], **kwargs): # that is needed for measuring, will unexpectedly fail. Also always adding a prebuilt theme # reduces the amount of setup that is needed to create a test suite Bazel target. Note that the # prebuilt theme will be also added to CDK test suites but shouldn't affect anything. - static_css = static_css + ["//src/material/prebuilt-themes:indigo-pink"] + static_css = static_css + [ + "//src/material/prebuilt-themes:indigo-pink", + "//src/material-experimental/mdc-theming:indigo_pink_prebuilt", + ] # Workaround for https://github.com/bazelbuild/rules_typescript/issues/301 # Since some of our tests depend on CSS files which are not part of the `ng_module` rule,