Skip to content

feat(cdk/testing): Allow custom querySelectorAll method #18178

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 14 commits into from
Mar 9, 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 package.json
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@
"husky": "^1.3.1",
"inquirer": "^6.2.0",
"jasmine-core": "^3.5.0",
"kagekiri": "^1.0.18",
"karma": "^4.4.1",
"karma-browserstack-launcher": "^1.3.0",
"karma-chrome-launcher": "^2.2.0",
Expand Down
1 change: 1 addition & 0 deletions rollup-globals.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ ROLLUP_GLOBALS = {
"@material/top-app-bar": "mdc.topAppBar",

# Third-party libraries.
"kagekiri": "kagekiri",
"moment": "moment",
"protractor": "protractor",
"rxjs": "rxjs",
Expand Down
10 changes: 9 additions & 1 deletion src/cdk/testing/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,15 @@ filegroup(

ng_web_test_suite(
name = "unit_tests",
deps = ["//src/cdk/testing/tests:unit_test_sources"],
# We need to load Kagekiri statically since it is not a named AMD module and needs to
# be manually configured through "require.js" which is used by "karma_web_test_suite".
static_files = [
"@npm//kagekiri",
],
deps = [
":require-config.js",
"//src/cdk/testing/tests:unit_test_sources",
],
)

e2e_test_suite(
Expand Down
32 changes: 24 additions & 8 deletions src/cdk/testing/protractor/protractor-harness-environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,34 @@
*/

import {HarnessEnvironment, HarnessLoader, TestElement} from '@angular/cdk/testing';
import {by, element as protractorElement, ElementFinder} from 'protractor';
import {by, element as protractorElement, ElementArrayFinder, ElementFinder} from 'protractor';
import {ProtractorElement} from './protractor-element';

/** Options to configure the environment. */
export interface ProtractorHarnessEnvironmentOptions {
/** The query function used to find DOM elements. */
queryFn: (selector: string, root: ElementFinder) => ElementArrayFinder;
}

/** The default environment options. */
const defaultEnvironmentOptions: ProtractorHarnessEnvironmentOptions = {
queryFn: (selector: string, root: ElementFinder) => root.all(by.css(selector))
};

/** A `HarnessEnvironment` implementation for Protractor. */
export class ProtractorHarnessEnvironment extends HarnessEnvironment<ElementFinder> {
protected constructor(rawRootElement: ElementFinder) {
/** The options for this environment. */
private _options: ProtractorHarnessEnvironmentOptions;

protected constructor(
rawRootElement: ElementFinder, options?: ProtractorHarnessEnvironmentOptions) {
super(rawRootElement);
this._options = {...defaultEnvironmentOptions, ...options};
}

/** Creates a `HarnessLoader` rooted at the document root. */
static loader(): HarnessLoader {
return new ProtractorHarnessEnvironment(protractorElement(by.css('body')));
static loader(options?: ProtractorHarnessEnvironmentOptions): HarnessLoader {
return new ProtractorHarnessEnvironment(protractorElement(by.css('body')), options);
}

async forceStabilize(): Promise<void> {}
Expand All @@ -37,15 +53,15 @@ export class ProtractorHarnessEnvironment extends HarnessEnvironment<ElementFind
}

protected createEnvironment(element: ElementFinder): HarnessEnvironment<ElementFinder> {
return new ProtractorHarnessEnvironment(element);
return new ProtractorHarnessEnvironment(element, this._options);
}

protected async getAllRawElements(selector: string): Promise<ElementFinder[]> {
const elementFinderArray = this.rawRootElement.all(by.css(selector));
const length = await elementFinderArray.count();
const elementArrayFinder = this._options.queryFn(selector, this.rawRootElement);
const length = await elementArrayFinder.count();
const elements: ElementFinder[] = [];
for (let i = 0; i < length; i++) {
elements.push(elementFinderArray.get(i));
elements.push(elementArrayFinder.get(i));
}
return elements;
}
Expand Down
7 changes: 7 additions & 0 deletions src/cdk/testing/require-config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Require.js is being used by the karma bazel rules and needs to be configured to properly
// load AMD modules which are not explicitly named in their output bundle.
require.config({
paths: {
'kagekiri': '/base/npm/node_modules/kagekiri/dist/kagekiri.umd.min',
}
});
37 changes: 28 additions & 9 deletions src/cdk/testing/testbed/testbed-harness-environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,31 +19,49 @@ import {takeWhile} from 'rxjs/operators';
import {TaskState, TaskStateZoneInterceptor} from './task-state-zone-interceptor';
import {UnitTestElement} from './unit-test-element';

/** Options to configure the environment. */
export interface TestbedHarnessEnvironmentOptions {
/** The query function used to find DOM elements. */
queryFn: (selector: string, root: Element) => Iterable<Element> | ArrayLike<Element>;
}

/** The default environment options. */
const defaultEnvironmentOptions: TestbedHarnessEnvironmentOptions = {
queryFn: (selector: string, root: Element) => root.querySelectorAll(selector)
};

/** A `HarnessEnvironment` implementation for Angular's Testbed. */
export class TestbedHarnessEnvironment extends HarnessEnvironment<Element> {
/** Whether the environment has been destroyed. */
private _destroyed = false;

/** Observable that emits whenever the test task state changes. */
private _taskState: Observable<TaskState>;

protected constructor(rawRootElement: Element, private _fixture: ComponentFixture<unknown>) {
/** The options for this environment. */
private _options: TestbedHarnessEnvironmentOptions;

protected constructor(rawRootElement: Element, private _fixture: ComponentFixture<unknown>,
options?: TestbedHarnessEnvironmentOptions) {
super(rawRootElement);
this._options = {...defaultEnvironmentOptions, ...options};
this._taskState = TaskStateZoneInterceptor.setup();
_fixture.componentRef.onDestroy(() => this._destroyed = true);
}

/** Creates a `HarnessLoader` rooted at the given fixture's root element. */
static loader(fixture: ComponentFixture<unknown>): HarnessLoader {
return new TestbedHarnessEnvironment(fixture.nativeElement, fixture);
static loader(fixture: ComponentFixture<unknown>, options?: TestbedHarnessEnvironmentOptions):
HarnessLoader {
return new TestbedHarnessEnvironment(fixture.nativeElement, fixture, options);
}

/**
* Creates a `HarnessLoader` at the document root. This can be used if harnesses are
* located outside of a fixture (e.g. overlays appended to the document body).
*/
static documentRootLoader(fixture: ComponentFixture<unknown>): HarnessLoader {
return new TestbedHarnessEnvironment(document.body, fixture);
static documentRootLoader(fixture: ComponentFixture<unknown>,
options?: TestbedHarnessEnvironmentOptions): HarnessLoader {
return new TestbedHarnessEnvironment(document.body, fixture, options);
}

/**
Expand All @@ -53,8 +71,9 @@ export class TestbedHarnessEnvironment extends HarnessEnvironment<Element> {
* of the fixture.
*/
static async harnessForFixture<T extends ComponentHarness>(
fixture: ComponentFixture<unknown>, harnessType: ComponentHarnessConstructor<T>): Promise<T> {
const environment = new TestbedHarnessEnvironment(fixture.nativeElement, fixture);
fixture: ComponentFixture<unknown>, harnessType: ComponentHarnessConstructor<T>,
options?: TestbedHarnessEnvironmentOptions): Promise<T> {
const environment = new TestbedHarnessEnvironment(fixture.nativeElement, fixture, options);
await environment.forceStabilize();
return environment.createComponentHarness(harnessType, fixture.nativeElement);
}
Expand Down Expand Up @@ -95,11 +114,11 @@ export class TestbedHarnessEnvironment extends HarnessEnvironment<Element> {
}

protected createEnvironment(element: Element): HarnessEnvironment<Element> {
return new TestbedHarnessEnvironment(element, this._fixture);
return new TestbedHarnessEnvironment(element, this._fixture, this._options);
}

protected async getAllRawElements(selector: string): Promise<Element[]> {
await this.forceStabilize();
return Array.from(this.rawRootElement.querySelectorAll(selector));
return Array.from(this._options.queryFn(selector, this.rawRootElement));
}
}
14 changes: 12 additions & 2 deletions src/cdk/testing/tests/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@ ng_module(
["**/*.ts"],
exclude = [
"**/*.spec.ts",
"**/*.spec.d.ts",
"harnesses/**",
],
),
assets = glob(["**/*.html"]),
deps = [
"//src/cdk/keycodes",
"//src/cdk/platform",
"@npm//@angular/forms",
],
)
Expand All @@ -30,22 +32,30 @@ ts_library(
ng_test_library(
name = "unit_test_sources",
srcs = glob(
["**/*.spec.ts"],
[
"**/*.spec.ts",
"**/*.spec.d.ts",
],
exclude = ["**/*.e2e.spec.ts"],
),
deps = [
":test_components",
":test_harnesses",
"//src/cdk/platform",
"//src/cdk/testing",
"//src/cdk/testing/private",
"//src/cdk/testing/testbed",
"@npm//@angular/platform-browser",
"@npm//kagekiri",
],
)

ng_e2e_test_library(
name = "e2e_test_sources",
srcs = glob(["**/*.e2e.spec.ts"]),
srcs = glob([
"**/*.e2e.spec.ts",
"**/*.spec.d.ts",
]),
deps = [
":test_harnesses",
"//src/cdk/testing",
Expand Down
3 changes: 3 additions & 0 deletions src/cdk/testing/tests/harnesses/main-component-harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ export class MainComponentHarness extends ComponentHarness {
this.locatorForAll(SubComponentHarness, SubComponentSpecialHarness);
readonly missingElementsAndHarnesses =
this.locatorFor('.not-found', SubComponentHarness.with({title: /not found/}));
readonly shadows = this.locatorForAll('.in-the-shadows');
readonly deepShadow = this.locatorFor(
'test-shadow-boundary test-sub-shadow-boundary > .in-the-shadows');

private _testTools = this.locatorFor(SubComponentHarness);

Expand Down
13 changes: 13 additions & 0 deletions src/cdk/testing/tests/kagekiri.spec.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* @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
*/

// Note: kagekiri is a dev dependency that is used only in our tests to test using a custom
// querySelector function. Do not use this in published code.
declare module 'kagekiri' {
export function querySelectorAll(selector: string, root: Element): NodeListOf<Element>;
}
34 changes: 33 additions & 1 deletion src/cdk/testing/tests/protractor.e2e.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,21 @@ import {
TestElement
} from '@angular/cdk/testing';
import {ProtractorHarnessEnvironment} from '@angular/cdk/testing/protractor';
import {browser} from 'protractor';
import {browser, by, element as protractorElement, ElementFinder} from 'protractor';
import {MainComponentHarness} from './harnesses/main-component-harness';
import {SubComponentHarness, SubComponentSpecialHarness} from './harnesses/sub-component-harness';

// Kagekiri is available globally in the browser. We declare it here so we can use it in the
// browser-side script passed to `by.js`.
// TODO(mmalerba): Replace with type-only import once TS 3.8 is available, see:
// https://devblogs.microsoft.com/typescript/announcing-typescript-3-8-beta/#type-only-imports-exports
declare const kagekiri: {
querySelectorAll: (selector: string, root: Element) => NodeListOf<Element>;
};

const piercingQueryFn = (selector: string, root: ElementFinder) => protractorElement.all(by.js(
(s: string, r: Element) => kagekiri.querySelectorAll(s, r), selector, root.getWebElement()));

describe('ProtractorHarnessEnvironment', () => {
beforeEach(async () => {
await browser.get('/component-harness');
Expand Down Expand Up @@ -460,6 +471,27 @@ describe('ProtractorHarnessEnvironment', () => {
}
});
});

describe('shadow DOM interaction', () => {
it('should not pierce shadow boundary by default', async () => {
const harness = await ProtractorHarnessEnvironment.loader()
.getHarness(MainComponentHarness);
expect(await harness.shadows()).toEqual([]);
});

it('should pierce shadow boundary when using piercing query', async () => {
const harness = await ProtractorHarnessEnvironment.loader({queryFn: piercingQueryFn})
.getHarness(MainComponentHarness);
const shadows = await harness.shadows();
expect(await Promise.all(shadows.map(el => el.text()))).toEqual(['Shadow 1', 'Shadow 2']);
});

it('should allow querying across shadow boundary', async () => {
const harness = await ProtractorHarnessEnvironment.loader({queryFn: piercingQueryFn})
.getHarness(MainComponentHarness);
expect(await (await harness.deepShadow()).text()).toBe('Shadow 2');
});
});
});

async function checkIsElement(result: ComponentHarness | TestElement, selector?: string) {
Expand Down
5 changes: 3 additions & 2 deletions src/cdk/testing/tests/test-components-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@ import {CommonModule} from '@angular/common';
import {NgModule} from '@angular/core';
import {FormsModule} from '@angular/forms';
import {TestMainComponent} from './test-main-component';
import {TestShadowBoundary, TestSubShadowBoundary} from './test-shadow-boundary';
import {TestSubComponent} from './test-sub-component';

@NgModule({
imports: [CommonModule, FormsModule],
declarations: [TestMainComponent, TestSubComponent],
exports: [TestMainComponent, TestSubComponent]
declarations: [TestMainComponent, TestSubComponent, TestShadowBoundary, TestSubShadowBoundary],
exports: [TestMainComponent, TestSubComponent, TestShadowBoundary, TestSubShadowBoundary]
})
export class TestComponentsModule {}
2 changes: 1 addition & 1 deletion src/cdk/testing/tests/test-main-component.html
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,4 @@ <h1 style="height: 100px; width: 200px;">Main Component</h1>
</button>
<span id="task-state-result" #taskStateResult></span>
</div>

<test-shadow-boundary *ngIf="_shadowDomSupported"></test-shadow-boundary>
2 changes: 2 additions & 0 deletions src/cdk/testing/tests/test-main-component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*/

import {ENTER} from '@angular/cdk/keycodes';
import {_supportsShadowDom} from '@angular/cdk/platform';
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Expand Down Expand Up @@ -42,6 +43,7 @@ export class TestMainComponent implements OnDestroy {
specialKey = '';
relativeX = 0;
relativeY = 0;
_shadowDomSupported = _supportsShadowDom();

@ViewChild('clickTestElement') clickTestElement: ElementRef<HTMLElement>;
@ViewChild('taskStateResult') taskStateResultElement: ElementRef<HTMLElement>;
Expand Down
30 changes: 30 additions & 0 deletions src/cdk/testing/tests/test-shadow-boundary.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/**
* @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 {ChangeDetectionStrategy, Component, ViewEncapsulation} from '@angular/core';

@Component({
selector: 'test-shadow-boundary',
template: `
<div class="in-the-shadows">Shadow 1</div>
<test-sub-shadow-boundary></test-sub-shadow-boundary>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
// tslint:disable-next-line:validate-decorators
encapsulation: ViewEncapsulation.ShadowDom,
})
export class TestShadowBoundary {}

@Component({
selector: 'test-sub-shadow-boundary',
template: '<div class="in-the-shadows">Shadow 2</div>',
changeDetection: ChangeDetectionStrategy.OnPush,
// tslint:disable-next-line:validate-decorators
encapsulation: ViewEncapsulation.ShadowDom,
})
export class TestSubShadowBoundary {}
Loading