Skip to content

Commit

Permalink
POC: per-harness zoneless knob
Browse files Browse the repository at this point in the history
  • Loading branch information
mmalerba committed Jul 10, 2024
1 parent fcc3d31 commit 185e251
Show file tree
Hide file tree
Showing 6 changed files with 146 additions and 44 deletions.
8 changes: 5 additions & 3 deletions src/cdk/testing/component-harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@ export interface LocatorFactory {
/**
* Waits for the given condition
*/
until(condition: () => boolean | Promise<boolean>): Promise<void>;
until(log: string, condition: () => boolean | Promise<boolean>): Promise<void>;

sleep(ms: number): Promise<void>;
}
Expand Down Expand Up @@ -411,8 +411,8 @@ export abstract class ComponentHarness {
return this.locatorFactory.sleep(ms);
}

protected async until(condition: () => boolean | Promise<boolean>) {
return this.locatorFactory.until(condition);
protected async until(log: string, condition: () => boolean | Promise<boolean>) {
return this.locatorFactory.until(log, condition);
}
}

Expand Down Expand Up @@ -467,6 +467,8 @@ export interface ComponentHarnessConstructor<T extends ComponentHarness> {
* for the Angular component.
*/
hostSelector: string;

readonly zoneless?: boolean;
}

/** A set of criteria that can be used to filter a list of `ComponentHarness` instances. */
Expand Down
35 changes: 21 additions & 14 deletions src/cdk/testing/harness-environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/

import {manualChangeDetection, parallel} from './change-detection';
import {parallel} from './change-detection';
import {
AsyncFactoryFn,
ComponentHarness,
Expand Down Expand Up @@ -54,11 +54,14 @@ export abstract class HarnessEnvironment<E> implements HarnessLoader, LocatorFac
}
private _rootElement: TestElement | undefined;

protected constructor(protected rawRootElement: E) {}
protected constructor(
protected rawRootElement: E,
protected zoneless = false,
) {}

// Implemented as part of the `LocatorFactory` interface.
documentRootLocatorFactory(): LocatorFactory {
return this.createEnvironment(this.getDocumentRoot());
return this.createEnvironment(this.getDocumentRoot(), this.zoneless);
}

// Implemented as part of the `LocatorFactory` interface.
Expand Down Expand Up @@ -97,19 +100,20 @@ export abstract class HarnessEnvironment<E> implements HarnessLoader, LocatorFac
await _assertResultFound(this.getAllRawElements(selector), [
_getDescriptionForHarnessLoaderQuery(selector),
]),
this.zoneless,
);
}

// Implemented as part of the `LocatorFactory` interface.
async harnessLoaderForOptional(selector: string): Promise<HarnessLoader | null> {
const elements = await this.getAllRawElements(selector);
return elements[0] ? this.createEnvironment(elements[0]) : null;
return elements[0] ? this.createEnvironment(elements[0], this.zoneless) : null;
}

// Implemented as part of the `LocatorFactory` interface.
async harnessLoaderForAll(selector: string): Promise<HarnessLoader[]> {
const elements = await this.getAllRawElements(selector);
return elements.map(element => this.createEnvironment(element));
return elements.map(element => this.createEnvironment(element, this.zoneless));
}

// Implemented as part of the `HarnessLoader` interface.
Expand Down Expand Up @@ -138,20 +142,23 @@ export abstract class HarnessEnvironment<E> implements HarnessLoader, LocatorFac
await _assertResultFound(this.getAllRawElements(selector), [
_getDescriptionForHarnessLoaderQuery(selector),
]),
this.zoneless,
);
}

// Implemented as part of the `HarnessLoader` interface.
async getAllChildLoaders(selector: string): Promise<HarnessLoader[]> {
return (await this.getAllRawElements(selector)).map(e => this.createEnvironment(e));
return (await this.getAllRawElements(selector)).map(e =>
this.createEnvironment(e, this.zoneless),
);
}

/** Creates a `ComponentHarness` for the given harness type with the given raw host element. */
protected createComponentHarness<T extends ComponentHarness>(
harnessType: ComponentHarnessConstructor<T>,
element: E,
): T {
return new harnessType(this.createEnvironment(element));
return new harnessType(this.createEnvironment(element, harnessType.zoneless));
}

// Part of LocatorFactory interface, subclasses will implement.
Expand All @@ -163,12 +170,12 @@ export abstract class HarnessEnvironment<E> implements HarnessLoader, LocatorFac
// Part of LocatorFactory interface, subclasses will implement.
abstract sleep(ms: number): Promise<void>;

async until(condition: () => boolean | Promise<boolean>) {
await manualChangeDetection(async () => {
while (!(await condition())) {
await this.sleep(0);
}
});
async until(log: string, condition: () => boolean | Promise<boolean>) {
console.log(`Waiting for condition: ${log} (PENDING...)`);
while (!(await condition())) {
await this.sleep(1);
}
console.log(`Waiting for condition: ${log} (COMPLETE!)`);
}

/** Gets the root element for the document. */
Expand All @@ -178,7 +185,7 @@ export abstract class HarnessEnvironment<E> implements HarnessLoader, LocatorFac
protected abstract createTestElement(element: E): TestElement;

/** Creates a `HarnessLoader` rooted at the given raw element. */
protected abstract createEnvironment(element: E): HarnessEnvironment<E>;
protected abstract createEnvironment(element: E, zoneless?: boolean): HarnessEnvironment<E>;

/**
* Gets a list of all elements matching the given selector under this environment's root element.
Expand Down
46 changes: 31 additions & 15 deletions src/cdk/testing/testbed/testbed-harness-environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,17 @@ const activeFixtures = new Set<ComponentFixture<unknown>>();
* Installs a handler for change detection batching status changes for a specific fixture.
* @param fixture The fixture to handle change detection batching for.
*/
function installAutoChangeDetectionStatusHandler(fixture: ComponentFixture<unknown>) {
function installAutoChangeDetectionStatusHandler(
fixture: ComponentFixture<unknown>,
zoneless: boolean,
) {
if (!activeFixtures.size) {
handleAutoChangeDetectionStatus(({isDisabled, onDetectChangesNow}) => {
disableAutoChangeDetection = isDisabled;
if (onDetectChangesNow) {
Promise.all(Array.from(activeFixtures).map(detectChanges)).then(onDetectChangesNow);
Promise.all(Array.from(activeFixtures).map(f => detectChanges(f, zoneless))).then(
onDetectChangesNow,
);
}
});
}
Expand All @@ -76,12 +81,14 @@ function isInFakeAsyncZone() {
* Triggers change detection for a specific fixture.
* @param fixture The fixture to trigger change detection for.
*/
async function detectChanges(fixture: ComponentFixture<unknown>) {
async function detectChanges(fixture: ComponentFixture<unknown>, zoneless: boolean) {
fixture.detectChanges();
if (isInFakeAsyncZone()) {
flush();
} else {
await fixture.whenStable();
if (!zoneless) {
if (isInFakeAsyncZone()) {
flush();
} else {
await fixture.whenStable();
}
}
}

Expand All @@ -103,14 +110,15 @@ export class TestbedHarnessEnvironment extends HarnessEnvironment<Element> {
rawRootElement: Element,
private _fixture: ComponentFixture<unknown>,
options?: TestbedHarnessEnvironmentOptions,
zoneless?: boolean,
) {
super(rawRootElement);
super(rawRootElement, zoneless);
this._options = {...defaultEnvironmentOptions, ...options};
if (typeof Zone !== 'undefined') {
this._taskState = TaskStateZoneInterceptor.setup();
}
this._stabilizeCallback = () => this.forceStabilize();
installAutoChangeDetectionStatusHandler(_fixture);
installAutoChangeDetectionStatusHandler(_fixture, this.zoneless);
_fixture.componentRef.onDestroy(() => {
uninstallAutoChangeDetectionStatusHandler(_fixture);
this._destroyed = true;
Expand All @@ -122,7 +130,7 @@ export class TestbedHarnessEnvironment extends HarnessEnvironment<Element> {
fixture: ComponentFixture<unknown>,
options?: TestbedHarnessEnvironmentOptions,
): HarnessLoader {
return new TestbedHarnessEnvironment(fixture.nativeElement, fixture, options);
return new TestbedHarnessEnvironment(fixture.nativeElement, fixture, options, true);
}

/**
Expand All @@ -133,7 +141,7 @@ export class TestbedHarnessEnvironment extends HarnessEnvironment<Element> {
fixture: ComponentFixture<unknown>,
options?: TestbedHarnessEnvironmentOptions,
): HarnessLoader {
return new TestbedHarnessEnvironment(document.body, fixture, options);
return new TestbedHarnessEnvironment(document.body, fixture, options, true);
}

/** Gets the native DOM element corresponding to the given TestElement. */
Expand All @@ -155,7 +163,12 @@ export class TestbedHarnessEnvironment extends HarnessEnvironment<Element> {
harnessType: ComponentHarnessConstructor<T>,
options?: TestbedHarnessEnvironmentOptions,
): Promise<T> {
const environment = new TestbedHarnessEnvironment(fixture.nativeElement, fixture, options);
const environment = new TestbedHarnessEnvironment(
fixture.nativeElement,
fixture,
options,
harnessType.zoneless,
);
await environment.forceStabilize();
return environment.createComponentHarness(harnessType, fixture.nativeElement);
}
Expand All @@ -171,7 +184,7 @@ export class TestbedHarnessEnvironment extends HarnessEnvironment<Element> {
throw Error('Harness is attempting to use a fixture that has already been destroyed.');
}

await detectChanges(this._fixture);
await detectChanges(this._fixture, this.zoneless);
}
}

Expand All @@ -180,6 +193,9 @@ export class TestbedHarnessEnvironment extends HarnessEnvironment<Element> {
* authors to wait for async tasks outside of the Angular zone.
*/
async waitForTasksOutsideAngular(): Promise<void> {
if (this.zoneless) {
return;
}
// If we run in the fake async zone, we run "flush" to run any scheduled tasks. This
// ensures that the harnesses behave inside of the FakeAsyncTestZone similar to the
// "AsyncTestZone" and the root zone (i.e. neither fakeAsync or async). Note that we
Expand Down Expand Up @@ -216,8 +232,8 @@ export class TestbedHarnessEnvironment extends HarnessEnvironment<Element> {
}

/** Creates a `HarnessLoader` rooted at the given raw element. */
protected createEnvironment(element: Element): HarnessEnvironment<Element> {
return new TestbedHarnessEnvironment(element, this._fixture, this._options);
protected createEnvironment(element: Element, zoneless = false): HarnessEnvironment<Element> {
return new TestbedHarnessEnvironment(element, this._fixture, this._options, zoneless);
}

/**
Expand Down
40 changes: 34 additions & 6 deletions src/cdk/testing/tests/harnesses/main-component-harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,16 +127,10 @@ export class MainComponentHarness extends ComponentHarness {

private _testTools = this.locatorFor(SubComponentHarness);

async isInitialized() {
await this.sleep(1000);
}

async increaseCounter(times: number) {
const button = await this.button();
for (let i = 0; i < times; i++) {
const oldCount = await (await this.asyncCounter()).text();
await button.click();
await this.until(async () => oldCount !== (await (await this.asyncCounter()).text()));
}
}

Expand Down Expand Up @@ -166,3 +160,37 @@ export class MainComponentHarness extends ComponentHarness {
return (await this.taskStateTestResult()).text();
}
}

export class MainComponentZonelessHarness extends MainComponentHarness {
static readonly zoneless = true;

async untilInitialized() {
await this.until(
'counter initialized',
async () => (await (await this.asyncCounter()).text()) !== '0',
);
}

override async increaseCounter(times: number) {
const button = await this.button();
for (let i = 0; i < times; i++) {
const oldCount = await (await this.asyncCounter()).text();
await button.click();
await this.until(
`counter incremented to ${Number(oldCount) + 1}`,
async () => oldCount !== (await (await this.asyncCounter()).text()),
);
}
}
}

export class BrokenMainComponentZonelessHarness extends MainComponentHarness {
static readonly zoneless = true;

async untilCounterIs100() {
return this.until(
'counter is 100',
async () => (await (await this.asyncCounter()).text()) === '100',
);
}
}
6 changes: 3 additions & 3 deletions src/cdk/testing/tests/test-main-component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@

import {ENTER} from '@angular/cdk/keycodes';
import {_supportsShadowDom} from '@angular/cdk/platform';
import {FormControl, FormsModule, ReactiveFormsModule} from '@angular/forms';
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Expand All @@ -19,6 +18,7 @@ import {
ViewChild,
ViewEncapsulation,
} from '@angular/core';
import {FormControl, FormsModule, ReactiveFormsModule} from '@angular/forms';
import {TestShadowBoundary} from './test-shadow-boundary';
import {TestSubComponent} from './test-sub-component';

Expand Down Expand Up @@ -71,7 +71,7 @@ export class TestMainComponent implements OnDestroy {
setTimeout(() => {
this.asyncCounter = 5;
this._cdr.markForCheck();
}, 1000);
}, 1);

this._fakeOverlayElement = document.createElement('div');
this._fakeOverlayElement.classList.add('fake-overlay');
Expand All @@ -88,7 +88,7 @@ export class TestMainComponent implements OnDestroy {
setTimeout(() => {
this.asyncCounter++;
this._cdr.markForCheck();
}, 500);
}, 1);
}

onKeyDown(event: KeyboardEvent) {
Expand Down
Loading

0 comments on commit 185e251

Please sign in to comment.