test(forms): Add utility functions and update test files to use them

Moves common helper functions into a shared test utility to reduce duplication.

(cherry picked from commit 318fefad30)
This commit is contained in:
SkyZeroZx
2026-09-01 12:17:43 -05:00
committed by Alex Rickabaugh
parent 13f3455a60
commit cc05deab72
10 changed files with 76 additions and 113 deletions
@@ -9,6 +9,7 @@
import {Injector, signal} from '@angular/core';
import {TestBed} from '@angular/core/testing';
import {applyWhenValue, debounce, form} from '@angular/forms/signals';
import {timeout} from '@angular/private/testing';
describe('debounce', () => {
describe('by duration', () => {
@@ -510,11 +511,6 @@ function options() {
return {injector: TestBed.inject(Injector)};
}
/** Returns a promise that will resolve after {@link durationInMilliseconds}. */
function timeout(durationInMilliseconds: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, durationInMilliseconds));
}
/** Returns a promise that will never resolve. */
function forever(): Promise<never> {
return new Promise(() => {});
@@ -11,6 +11,7 @@ import {TestBed} from '@angular/core/testing';
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
import {form, FormRoot} from '../../public_api';
import {act} from '@angular/private/testing';
@Component({
template: `
@@ -143,11 +144,3 @@ describe('FormRoot', () => {
expect(component.submitted).toBeTrue();
});
});
function act<T>(fn: () => T): T {
try {
return fn();
} finally {
TestBed.tick();
}
}
@@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.dev/license
*/
import {ApplicationRef, Component, input, model, signal} from '@angular/core';
import {Component, input, model, signal} from '@angular/core';
import {TestBed} from '@angular/core/testing';
import {
form,
@@ -17,6 +17,7 @@ import {
type FormValueControl,
type ValidationError,
} from '../../public_api';
import {act} from '@angular/private/testing';
describe('parse errors', () => {
it('should only pass parse errors through to the originating custom control', async () => {
@@ -410,9 +411,3 @@ class TestNumberInput implements FormValueControl<number | null> {
},
});
}
async function act<T>(fn: () => T): Promise<T> {
const result = fn();
await TestBed.inject(ApplicationRef).whenStable();
return result;
}
@@ -11,6 +11,7 @@ import {TestBed} from '@angular/core/testing';
import {FormControl} from '@angular/forms';
import {compatForm} from '../../compat';
import {FormField} from '../../public_api';
import {act} from '@angular/private/testing';
describe('compatForm with [formField] directive', () => {
beforeEach(() => {
@@ -87,11 +88,3 @@ describe('compatForm with [formField] directive', () => {
expect(fixture.componentInstance.f().value().species).toBe('cat');
});
});
function act<T>(fn: () => T): T {
try {
return fn();
} finally {
TestBed.tick();
}
}
@@ -15,6 +15,7 @@ import {
} from '@angular/core';
import {TestBed} from '@angular/core/testing';
import {form, type FieldTree} from '../../public_api';
import {act} from '@angular/private/testing';
describe('field proxy', () => {
beforeEach(() => {
@@ -59,11 +60,3 @@ describe('field proxy', () => {
expect(fix.nativeElement.querySelectorAll('p').length).toBe(2);
});
});
function act<T>(fn: () => T): T {
try {
return fn();
} finally {
TestBed.tick();
}
}
+24 -38
View File
@@ -6,20 +6,12 @@
* found in the LICENSE file at https://angular.dev/license
*/
import {
ApplicationRef,
Component,
inject,
input,
model,
signal,
viewChild,
type ElementRef,
} from '@angular/core';
import {Component, inject, input, model, signal, viewChild, type ElementRef} from '@angular/core';
import {TestBed} from '@angular/core/testing';
import {FormControl} from '@angular/forms';
import {compatForm} from '../../compat';
import {FormField, form, type Field, type FieldTree} from '../../public_api';
import {act} from '@angular/private/testing';
describe('FieldState focus behavior', () => {
it('should focus a native control', async () => {
@@ -31,11 +23,11 @@ describe('FieldState focus behavior', () => {
readonly f = form(signal(''));
}
const fixture = await act(() => TestBed.createComponent(TestCmp));
const fixture = act(() => TestBed.createComponent(TestCmp));
const input = fixture.nativeElement.firstChild;
expect(document.activeElement).not.toBe(input);
await act(() => fixture.componentInstance.f().focusBoundControl());
act(() => fixture.componentInstance.f().focusBoundControl());
expect(document.activeElement).toBe(input);
});
@@ -64,10 +56,10 @@ describe('FieldState focus behavior', () => {
readonly f = form(signal(''));
}
const fixture = await act(() => TestBed.createComponent(TestCmp));
const fixture = act(() => TestBed.createComponent(TestCmp));
const customControl = fixture.nativeElement.firstChild as HTMLInputElement;
await act(() => fixture.componentInstance.f().focusBoundControl());
act(() => fixture.componentInstance.f().focusBoundControl());
expect(focusCalled).toBeTrue();
expect(document.activeElement).not.toBe(customControl);
expect(document.activeElement).toBe(customControl.querySelector('input'));
@@ -125,10 +117,10 @@ describe('FieldState focus behavior', () => {
readonly f = form(signal(''));
}
const fixture = await act(() => TestBed.createComponent(TestCmp));
const fixture = act(() => TestBed.createComponent(TestCmp));
const customControl = fixture.nativeElement.firstChild as HTMLInputElement;
await act(() => fixture.componentInstance.f().focusBoundControl());
act(() => fixture.componentInstance.f().focusBoundControl());
expect(document.activeElement).toBe(customControl);
});
@@ -147,16 +139,16 @@ describe('FieldState focus behavior', () => {
showFirst = signal(false);
}
const fixture = await act(() => TestBed.createComponent(TestCmp));
const fixture = act(() => TestBed.createComponent(TestCmp));
const input2 = fixture.nativeElement.querySelector('#input2');
await act(() => fixture.componentInstance.f().focusBoundControl());
act(() => fixture.componentInstance.f().focusBoundControl());
expect(document.activeElement).toBe(input2);
await act(() => fixture.componentInstance.showFirst.set(true));
act(() => fixture.componentInstance.showFirst.set(true));
const input1 = fixture.nativeElement.querySelector('#input1');
await act(() => fixture.componentInstance.f().focusBoundControl());
act(() => fixture.componentInstance.f().focusBoundControl());
expect(document.activeElement).toBe(input1);
});
@@ -181,10 +173,10 @@ describe('FieldState focus behavior', () => {
readonly f = form(signal({child1: '', child2: ''}));
}
const fixture = await act(() => TestBed.createComponent(TestCmp));
const fixture = act(() => TestBed.createComponent(TestCmp));
const child2 = fixture.nativeElement.querySelector('#child2');
await act(() => fixture.componentInstance.f().focusBoundControl());
act(() => fixture.componentInstance.f().focusBoundControl());
expect(document.activeElement).toBe(child2);
});
@@ -197,11 +189,11 @@ describe('FieldState focus behavior', () => {
readonly f = compatForm(signal(new FormControl('', {nonNullable: true})));
}
const fixture = await act(() => TestBed.createComponent(TestCmp));
const fixture = act(() => TestBed.createComponent(TestCmp));
const input = fixture.nativeElement.firstChild;
expect(document.activeElement).not.toBe(input);
await act(() => fixture.componentInstance.f().focusBoundControl());
act(() => fixture.componentInstance.f().focusBoundControl());
expect(document.activeElement).toBe(input);
});
@@ -222,10 +214,10 @@ describe('FieldState focus behavior', () => {
readonly f = form(signal(''));
}
const fixture = await act(() => TestBed.createComponent(TestCmp));
const fixture = act(() => TestBed.createComponent(TestCmp));
const focusedEl = document.activeElement;
await act(() => fixture.componentInstance.f().focusBoundControl());
act(() => fixture.componentInstance.f().focusBoundControl());
expect(document.activeElement).toBe(focusedEl);
});
@@ -253,11 +245,11 @@ describe('FieldState focus behavior', () => {
readonly f = form(signal(''));
}
const fixture = await act(() => TestBed.createComponent(TestCmp));
const fixture = act(() => TestBed.createComponent(TestCmp));
const nativeInput = fixture.nativeElement.querySelector('custom-control > input');
expect(nativeInput).toBeTruthy();
await act(() => fixture.componentInstance.f().focusBoundControl());
act(() => fixture.componentInstance.f().focusBoundControl());
expect(document.activeElement).toBe(nativeInput);
});
@@ -270,12 +262,12 @@ describe('FieldState focus behavior', () => {
readonly f = form(signal(''));
}
const fixture = await act(() => TestBed.createComponent(TestCmp));
const fixture = act(() => TestBed.createComponent(TestCmp));
const input = fixture.nativeElement.firstChild as HTMLInputElement;
const focusSpy = spyOn(input, 'focus');
await act(() => fixture.componentInstance.f().focusBoundControl({preventScroll: true}));
act(() => fixture.componentInstance.f().focusBoundControl({preventScroll: true}));
expect(focusSpy).toHaveBeenCalledWith({preventScroll: true});
});
@@ -302,15 +294,9 @@ describe('FieldState focus behavior', () => {
readonly f = form(signal(''));
}
const fixture = await act(() => TestBed.createComponent(TestCmp));
const fixture = act(() => TestBed.createComponent(TestCmp));
await act(() => fixture.componentInstance.f().focusBoundControl({preventScroll: true}));
act(() => fixture.componentInstance.f().focusBoundControl({preventScroll: true}));
expect(receivedOptions).toEqual({preventScroll: true});
});
});
async function act<T>(fn: () => T): Promise<T> {
const result = fn();
await TestBed.inject(ApplicationRef).whenStable();
return result;
}
@@ -67,6 +67,7 @@ import {
type ValidationError,
type WithOptionalFieldTree,
} from '../../public_api';
import {act} from '@angular/private/testing';
import {InputValidityMonitor} from '../../src/directive/input_validity_monitor';
import {TestInputValidityMonitor} from './test_input_validity_monitor';
@@ -6541,11 +6542,3 @@ function setupRadioWithBindingsGroup() {
return {cmp, inputA, inputB, inputC, ABC};
}
function act<T>(fn: () => T): T {
try {
return fn();
} finally {
TestBed.tick();
}
}
@@ -7,7 +7,6 @@
*/
import {
ApplicationRef,
ChangeDetectionStrategy,
Component,
Directive,
@@ -56,6 +55,7 @@ import {
WithOptionalFieldTree,
transformedValue,
} from '@angular/forms/signals';
import {act, actAsync} from '@angular/private/testing';
describe('ControlValueAccessor', () => {
beforeEach(() => {
@@ -1558,19 +1558,3 @@ describe('ControlValueAccessor', () => {
});
});
});
function act<T>(fn: () => T): T {
try {
return fn();
} finally {
TestBed.tick();
}
}
async function actAsync<T>(fn: () => T): Promise<T> {
try {
return fn();
} finally {
await TestBed.inject(ApplicationRef).whenStable();
}
}
@@ -9,10 +9,10 @@
import {Component, Injector, inject, provideZonelessChangeDetection, signal} from '@angular/core';
import {TestBed} from '@angular/core/testing';
import {FormControl, FormGroup, ReactiveFormsModule} from '@angular/forms';
import {disabled} from '@angular/forms/signals';
import {disabled, FormField} from '@angular/forms/signals';
import {SignalFormControl} from '../../compat';
import {FormField} from '../../src/directive/form_field';
import {act} from '@angular/private/testing';
describe('SignalFormControl (web)', () => {
beforeEach(() => {
@@ -24,7 +24,6 @@ describe('SignalFormControl (web)', () => {
it('binds to formField directive', () => {
@Component({
standalone: true,
imports: [ReactiveFormsModule, FormField],
template: `<input [formField]="signalControl.fieldTree" />`,
})
@@ -51,7 +50,6 @@ describe('SignalFormControl (web)', () => {
it('binds inside nested FormGroup via formGroupName', () => {
@Component({
standalone: true,
imports: [ReactiveFormsModule, FormField],
template: `
<div [formGroup]="group">
@@ -90,7 +88,6 @@ describe('SignalFormControl (web)', () => {
it('should unregister disabled callback when directive is destroyed', () => {
@Component({
standalone: true,
imports: [ReactiveFormsModule],
template: `
@if (showInput()) {
@@ -123,11 +120,3 @@ describe('SignalFormControl (web)', () => {
}).not.toThrow();
});
});
function act<T>(fn: () => T): T {
try {
return fn();
} finally {
TestBed.tick();
}
}
+42 -1
View File
@@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.dev/license
*/
import {TestBed} from '../../../core/testing';
import {ɵresetJitOptions as resetJitOptions} from '@angular/core';
import {ApplicationRef, ɵresetJitOptions as resetJitOptions} from '@angular/core';
/**
* Wraps a function in a new function which sets up document and HTML for running a test.
@@ -298,3 +298,44 @@ export async function waitFor<T>(
await new Promise((resolve) => void realSetTimeout(resolve, interval));
}
}
/**
* Executes a function and waits for the application to become stable.
*
* Use this when the action triggers asynchronous work
* (e.g. a `resource` loader) that must complete before assertions run.
*
* @example
* ```ts
* const res = await actAsync(() => resource({ loader: fetchUser, injector }));
* expect(res.status()).toBe('resolved');
* ```
*/
export async function actAsync<T>(fn: () => T): Promise<T> {
const result = fn();
await TestBed.inject(ApplicationRef).whenStable();
return result;
}
/**
* Executes a function and flushes the Angular scheduler synchronously.
*
* Use this when the action triggers synchronous state
* changes that need to be flushed through the scheduler before assertions run.
*
* @example
* ```ts
* const fixture = act(() => TestBed.createComponent(TestComp));
* expect(fixture.nativeElement.textContent).toBe('initial');
*
* act(() => fixture.componentInstance.value.set('updated'));
* expect(fixture.nativeElement.textContent).toBe('updated');
* ```
*/
export function act<T>(fn: () => T): T {
try {
return fn();
} finally {
TestBed.tick();
}
}