Skip to content

Commit ecc72e1

Browse files
committed
feat(cdk/testing): Allow custom querySelectorAll method
This allows the user to drop in a shadow-piercing version of `querySelectorAll` to handle components that use `ViewEncapsulation.ShadowDom`
1 parent 149669e commit ecc72e1

16 files changed

+130
-20
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@
112112
"husky": "^1.3.1",
113113
"inquirer": "^6.2.0",
114114
"jasmine-core": "^3.3.0",
115+
"kagekiri": "^1.0.16",
115116
"karma": "^4.4.1",
116117
"karma-browserstack-launcher": "^1.3.0",
117118
"karma-chrome-launcher": "^2.2.0",

rollup-globals.bzl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ ROLLUP_GLOBALS = {
6969
"@material/top-app-bar": "mdc.topAppBar",
7070

7171
# Third-party libraries.
72+
"kagekiri": "kagekiri",
7273
"moment": "moment",
7374
"protractor": "protractor",
7475
"rxjs": "rxjs",

src/cdk/testing/BUILD.bazel

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,15 @@ filegroup(
2626

2727
ng_web_test_suite(
2828
name = "unit_tests",
29-
deps = ["//src/cdk/testing/tests:unit_test_sources"],
29+
# We need to load Kagekiri statically since it is not a named AMD module and needs to
30+
# be manually configured through "require.js" which is used by "karma_web_test_suite".
31+
static_files = [
32+
"@npm//kagekiri",
33+
],
34+
deps = [
35+
":require-config.js",
36+
"//src/cdk/testing/tests:unit_test_sources"
37+
],
3038
)
3139

3240
e2e_test_suite(

src/cdk/testing/kagekiri.d.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
declare module 'kagekiri' {
10+
export function querySelectorAll(selector: string, root: Element);
11+
}

src/cdk/testing/protractor/protractor-harness-environment.ts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,30 @@
77
*/
88

99
import {HarnessEnvironment, HarnessLoader, TestElement} from '@angular/cdk/testing';
10-
import {by, element as protractorElement, ElementFinder} from 'protractor';
10+
import {
11+
by,
12+
element as protractorElement,
13+
ElementArrayFinder,
14+
ElementFinder,
15+
Locator
16+
} from 'protractor';
1117
import {ProtractorElement} from './protractor-element';
1218

19+
/** The default query function that respects shadow boundaries. */
20+
const defaultQueryFn = (selector: string, root: ElementFinder) => root.all(by.css(selector));
21+
1322
/** A `HarnessEnvironment` implementation for Protractor. */
1423
export class ProtractorHarnessEnvironment extends HarnessEnvironment<ElementFinder> {
15-
protected constructor(rawRootElement: ElementFinder) {
24+
protected constructor(rawRootElement: ElementFinder,
25+
private _queryFn: (selector: string, root: ElementFinder) => ElementArrayFinder =
26+
defaultQueryFn) {
1627
super(rawRootElement);
1728
}
1829

1930
/** Creates a `HarnessLoader` rooted at the document root. */
20-
static loader(): HarnessLoader {
21-
return new ProtractorHarnessEnvironment(protractorElement(by.css('body')));
31+
static loader(queryFn?: (selector: string, root: ElementFinder) => ElementArrayFinder):
32+
HarnessLoader {
33+
return new ProtractorHarnessEnvironment(protractorElement(by.css('body')), queryFn);
2234
}
2335

2436
async forceStabilize(): Promise<void> {}
@@ -37,11 +49,11 @@ export class ProtractorHarnessEnvironment extends HarnessEnvironment<ElementFind
3749
}
3850

3951
protected createEnvironment(element: ElementFinder): HarnessEnvironment<ElementFinder> {
40-
return new ProtractorHarnessEnvironment(element);
52+
return new ProtractorHarnessEnvironment(element, this._queryFn);
4153
}
4254

4355
protected async getAllRawElements(selector: string): Promise<ElementFinder[]> {
44-
const elementFinderArray = this.rawRootElement.all(by.css(selector));
56+
const elementFinderArray = this._queryFn(selector, this.rawRootElement);
4557
const length = await elementFinderArray.count();
4658
const elements: ElementFinder[] = [];
4759
for (let i = 0; i < length; i++) {

src/cdk/testing/require-config.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// Require.js is being used by the karma bazel rules and needs to be configured to properly
2+
// load AMD modules which are not explicitly named in their output bundle.
3+
require.config({
4+
paths: {
5+
'kagekiri': '/base/npm/node_modules/kagekiri/dist/kagekiri.umd.min',
6+
}
7+
});

src/cdk/testing/testbed/testbed-harness-environment.ts

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import {takeWhile} from 'rxjs/operators';
1919
import {TaskState, TaskStateZoneInterceptor} from './task-state-zone-interceptor';
2020
import {UnitTestElement} from './unit-test-element';
2121

22+
/** The default query function that respects shadow boundaries. */
23+
const defaultQueryFn = (selector: string, root: Element) => root.querySelectorAll(selector);
2224

2325
/** A `HarnessEnvironment` implementation for Angular's Testbed. */
2426
export class TestbedHarnessEnvironment extends HarnessEnvironment<Element> {
@@ -27,23 +29,26 @@ export class TestbedHarnessEnvironment extends HarnessEnvironment<Element> {
2729
/** Observable that emits whenever the test task state changes. */
2830
private _taskState: Observable<TaskState>;
2931

30-
protected constructor(rawRootElement: Element, private _fixture: ComponentFixture<unknown>) {
32+
protected constructor(rawRootElement: Element, private _fixture: ComponentFixture<unknown>,
33+
private _queryFn: (selector: string, root: Element) => ArrayLike<Element> = defaultQueryFn) {
3134
super(rawRootElement);
3235
this._taskState = TaskStateZoneInterceptor.setup();
3336
_fixture.componentRef.onDestroy(() => this._destroyed = true);
3437
}
3538

3639
/** Creates a `HarnessLoader` rooted at the given fixture's root element. */
37-
static loader(fixture: ComponentFixture<unknown>): HarnessLoader {
38-
return new TestbedHarnessEnvironment(fixture.nativeElement, fixture);
40+
static loader(fixture: ComponentFixture<unknown>,
41+
queryFn?: (selector: string, root: Element) => ArrayLike<Element>): HarnessLoader {
42+
return new TestbedHarnessEnvironment(fixture.nativeElement, fixture, queryFn);
3943
}
4044

4145
/**
4246
* Creates a `HarnessLoader` at the document root. This can be used if harnesses are
4347
* located outside of a fixture (e.g. overlays appended to the document body).
4448
*/
45-
static documentRootLoader(fixture: ComponentFixture<unknown>): HarnessLoader {
46-
return new TestbedHarnessEnvironment(document.body, fixture);
49+
static documentRootLoader(fixture: ComponentFixture<unknown>,
50+
queryFn?: (selector: string, root: Element) => ArrayLike<Element>): HarnessLoader {
51+
return new TestbedHarnessEnvironment(document.body, fixture, queryFn);
4752
}
4853

4954
/**
@@ -53,8 +58,9 @@ export class TestbedHarnessEnvironment extends HarnessEnvironment<Element> {
5358
* of the fixture.
5459
*/
5560
static async harnessForFixture<T extends ComponentHarness>(
56-
fixture: ComponentFixture<unknown>, harnessType: ComponentHarnessConstructor<T>): Promise<T> {
57-
const environment = new TestbedHarnessEnvironment(fixture.nativeElement, fixture);
61+
fixture: ComponentFixture<unknown>, harnessType: ComponentHarnessConstructor<T>,
62+
queryFn?: (selector: string, root: Element) => ArrayLike<Element>): Promise<T> {
63+
const environment = new TestbedHarnessEnvironment(fixture.nativeElement, fixture, queryFn);
5864
await environment.forceStabilize();
5965
return environment.createComponentHarness(harnessType, fixture.nativeElement);
6066
}
@@ -95,11 +101,11 @@ export class TestbedHarnessEnvironment extends HarnessEnvironment<Element> {
95101
}
96102

97103
protected createEnvironment(element: Element): HarnessEnvironment<Element> {
98-
return new TestbedHarnessEnvironment(element, this._fixture);
104+
return new TestbedHarnessEnvironment(element, this._fixture, this._queryFn);
99105
}
100106

101107
protected async getAllRawElements(selector: string): Promise<Element[]> {
102108
await this.forceStabilize();
103-
return Array.from(this.rawRootElement.querySelectorAll(selector));
109+
return Array.from(this._queryFn(selector, this.rawRootElement));
104110
}
105111
}

src/cdk/testing/tests/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ ng_test_library(
3939
"//src/cdk/testing",
4040
"//src/cdk/testing/private",
4141
"//src/cdk/testing/testbed",
42+
"@npm//kagekiri",
4243
],
4344
)
4445

src/cdk/testing/tests/harnesses/main-component-harness.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ export class MainComponentHarness extends ComponentHarness {
8181
this.locatorForAll(SubComponentHarness, SubComponentSpecialHarness);
8282
readonly missingElementsAndHarnesses =
8383
this.locatorFor('.not-found', SubComponentHarness.with({title: /not found/}));
84+
readonly shadows = this.locatorForAll('.in-the-shadows');
85+
readonly deepShadow = this.locatorFor('.in-the-shadows + test-shadow-boundary > .in-the-shadows');
8486

8587
private _testTools = this.locatorFor(SubComponentHarness);
8688

src/cdk/testing/tests/test-components-module.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,12 @@ import {CommonModule} from '@angular/common';
1010
import {NgModule} from '@angular/core';
1111
import {FormsModule} from '@angular/forms';
1212
import {TestMainComponent} from './test-main-component';
13+
import {TestShadowBoundary} from './test-shadow-boundary';
1314
import {TestSubComponent} from './test-sub-component';
1415

1516
@NgModule({
1617
imports: [CommonModule, FormsModule],
17-
declarations: [TestMainComponent, TestSubComponent],
18-
exports: [TestMainComponent, TestSubComponent]
18+
declarations: [TestMainComponent, TestSubComponent, TestShadowBoundary],
19+
exports: [TestMainComponent, TestSubComponent, TestShadowBoundary]
1920
})
2021
export class TestComponentsModule {}

src/cdk/testing/tests/test-main-component.html

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,9 @@ <h1 style="height: 100px; width: 200px;">Main Component</h1>
3232
</button>
3333
<span id="task-state-result" #taskStateResult></span>
3434
</div>
35-
35+
<test-shadow-boundary>
36+
<div class="in-the-shadows">Shadow 1</div>
37+
<test-shadow-boundary>
38+
<div class="in-the-shadows">Shadow 2</div>
39+
</test-shadow-boundary>
40+
</test-shadow-boundary>
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import {ChangeDetectionStrategy, Component, ViewEncapsulation} from '@angular/core';
2+
3+
@Component({
4+
selector: 'test-shadow-boundary',
5+
template: '<ng-content></ng-content>',
6+
changeDetection: ChangeDetectionStrategy.OnPush,
7+
// tslint:disable-next-line:validate-decorators
8+
encapsulation: ViewEncapsulation.ShadowDom,
9+
})
10+
export class TestShadowBoundary {}

src/cdk/testing/tests/testbed.spec.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {MainComponentHarness} from './harnesses/main-component-harness';
1212
import {SubComponentHarness, SubComponentSpecialHarness} from './harnesses/sub-component-harness';
1313
import {TestComponentsModule} from './test-components-module';
1414
import {TestMainComponent} from './test-main-component';
15+
import {querySelectorAll as piercingQuerySelectorAll} from 'kagekiri';
1516

1617
function activeElementText() {
1718
return document.activeElement && (document.activeElement as HTMLElement).innerText || '';
@@ -492,6 +493,27 @@ describe('TestbedHarnessEnvironment', () => {
492493
}
493494
});
494495
});
496+
497+
describe('shadow DOM interaction', () => {
498+
it('should not pierce shadow boundary by default', async () => {
499+
const harness = await TestbedHarnessEnvironment
500+
.harnessForFixture(fixture, MainComponentHarness);
501+
expect(await harness.shadows()).toEqual([]);
502+
});
503+
504+
it('should pierce shadow boundary when using piercing query', async () => {
505+
const harness = await TestbedHarnessEnvironment
506+
.harnessForFixture(fixture, MainComponentHarness, piercingQuerySelectorAll);
507+
const shadows = await harness.shadows();
508+
expect(await Promise.all(shadows.map(el => el.text()))).toEqual(['Shadow 1', 'Shadow 2']);
509+
});
510+
511+
it('should allow querying across shadow boundary', async () => {
512+
const harness = await TestbedHarnessEnvironment
513+
.harnessForFixture(fixture, MainComponentHarness, piercingQuerySelectorAll);
514+
expect(await (await harness.deepShadow()).text()).toBe('Shadow 2');
515+
});
516+
});
495517
});
496518

497519
async function checkIsElement(result: ComponentHarness | TestElement, selector?: string) {

test/karma-system-config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ System.config({
66
'rxjs': 'node:rxjs',
77
'tslib': 'node:tslib/tslib.js',
88
'moment': 'node:moment/min/moment-with-locales.min.js',
9+
'kagekiri': 'node:kagekiri/dist/kagekiri.umd.min.js',
910

1011
// MDC Web
1112
'@material/animation': 'node:@material/animation/dist/mdc.animation.js',

test/karma.conf.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ module.exports = config => {
4545
watched: false
4646
},
4747
{pattern: 'node_modules/@material/*/dist/*', included: false, watched: false},
48+
{pattern: 'node_modules/kagekiri/**', included: false, watched: false},
4849

4950
// Include all Angular dependencies
5051
{pattern: 'node_modules/@angular/**/*', included: false, watched: false},

yarn.lock

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3526,6 +3526,11 @@ cssesc@^0.1.0:
35263526
resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-0.1.0.tgz#c814903e45623371a0477b40109aaafbeeaddbb4"
35273527
integrity sha1-yBSQPkViM3GgR3tAEJqq++6t27Q=
35283528

3529+
cssesc@^3.0.0:
3530+
version "3.0.0"
3531+
resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
3532+
integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==
3533+
35293534
csv-streamify@^3.0.4:
35303535
version "3.0.4"
35313536
resolved "https://registry.yarnpkg.com/csv-streamify/-/csv-streamify-3.0.4.tgz#4cb614c57e3f299cca17b63fdcb4ad167777f47a"
@@ -6731,6 +6736,13 @@ jws@^3.0.0, jws@^3.1.4, jws@^3.1.5:
67316736
jwa "^1.1.5"
67326737
safe-buffer "^5.0.1"
67336738

6739+
kagekiri@^1.0.16:
6740+
version "1.0.16"
6741+
resolved "https://registry.yarnpkg.com/kagekiri/-/kagekiri-1.0.16.tgz#b4e0aeb8d57fe2e8d8f84f0d099fe35d21c529ed"
6742+
integrity sha512-/F1UiJ1CpRIOqJq8o0z9r6By2Rv2B+rC37fFbcYnKTWJ4JDXmDaEMer8NAm3afWwgRrJgkrQldXy3BL1wToMlg==
6743+
dependencies:
6744+
postcss-selector-parser nolanlawson/postcss-selector-parser#util-deprecate
6745+
67346746
karma-browserstack-launcher@^1.3.0:
67356747
version "1.3.0"
67366748
resolved "https://registry.yarnpkg.com/karma-browserstack-launcher/-/karma-browserstack-launcher-1.3.0.tgz#61fe3d36b1cf10681e40f9d874bf37271fb1c674"
@@ -8971,6 +8983,15 @@ postcss-selector-parser@^3.1.0:
89718983
indexes-of "^1.0.1"
89728984
uniq "^1.0.1"
89738985

8986+
"postcss-selector-parser@github:nolanlawson/postcss-selector-parser#util-deprecate":
8987+
version "6.0.2"
8988+
resolved "https://codeload.github.com/nolanlawson/postcss-selector-parser/tar.gz/75e38f7a7db8d58fb033ab7d24414b55d81e5b4f"
8989+
dependencies:
8990+
cssesc "^3.0.0"
8991+
indexes-of "^1.0.1"
8992+
uniq "^1.0.1"
8993+
util-deprecate "^1.0.2"
8994+
89748995
postcss-syntax@^0.36.2:
89758996
version "0.36.2"
89768997
resolved "https://registry.yarnpkg.com/postcss-syntax/-/postcss-syntax-0.36.2.tgz#f08578c7d95834574e5593a82dfbfa8afae3b51c"
@@ -11717,7 +11738,7 @@ useragent@2.3.0:
1171711738
lru-cache "4.1.x"
1171811739
tmp "0.0.x"
1171911740

11720-
util-deprecate@^1.0.1, util-deprecate@~1.0.1:
11741+
util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1:
1172111742
version "1.0.2"
1172211743
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
1172311744
integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=

0 commit comments

Comments
 (0)