diff --git a/packages/forms/signals/test/node/api/debounce.spec.ts b/packages/forms/signals/test/node/api/debounce.spec.ts index 2fa9f5e9d27..eb0112d31fe 100644 --- a/packages/forms/signals/test/node/api/debounce.spec.ts +++ b/packages/forms/signals/test/node/api/debounce.spec.ts @@ -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 { - return new Promise((resolve) => setTimeout(resolve, durationInMilliseconds)); -} - /** Returns a promise that will never resolve. */ function forever(): Promise { return new Promise(() => {}); diff --git a/packages/forms/signals/test/node/form_root.spec.ts b/packages/forms/signals/test/node/form_root.spec.ts index 19bd60b45f8..cfda352a7b6 100644 --- a/packages/forms/signals/test/node/form_root.spec.ts +++ b/packages/forms/signals/test/node/form_root.spec.ts @@ -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(fn: () => T): T { - try { - return fn(); - } finally { - TestBed.tick(); - } -} diff --git a/packages/forms/signals/test/node/parse_errors.spec.ts b/packages/forms/signals/test/node/parse_errors.spec.ts index 1b366e92a0d..fd4ef1333a9 100644 --- a/packages/forms/signals/test/node/parse_errors.spec.ts +++ b/packages/forms/signals/test/node/parse_errors.spec.ts @@ -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 { }, }); } - -async function act(fn: () => T): Promise { - const result = fn(); - await TestBed.inject(ApplicationRef).whenStable(); - return result; -} diff --git a/packages/forms/signals/test/web/compat_form.spec.ts b/packages/forms/signals/test/web/compat_form.spec.ts index 938ed703dea..6a86fdcaf9f 100644 --- a/packages/forms/signals/test/web/compat_form.spec.ts +++ b/packages/forms/signals/test/web/compat_form.spec.ts @@ -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(fn: () => T): T { - try { - return fn(); - } finally { - TestBed.tick(); - } -} diff --git a/packages/forms/signals/test/web/field_proxy.spec.ts b/packages/forms/signals/test/web/field_proxy.spec.ts index 19c50fe518b..7274f952028 100644 --- a/packages/forms/signals/test/web/field_proxy.spec.ts +++ b/packages/forms/signals/test/web/field_proxy.spec.ts @@ -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(fn: () => T): T { - try { - return fn(); - } finally { - TestBed.tick(); - } -} diff --git a/packages/forms/signals/test/web/focus.spec.ts b/packages/forms/signals/test/web/focus.spec.ts index 998b6b24558..16274401ccb 100644 --- a/packages/forms/signals/test/web/focus.spec.ts +++ b/packages/forms/signals/test/web/focus.spec.ts @@ -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(fn: () => T): Promise { - const result = fn(); - await TestBed.inject(ApplicationRef).whenStable(); - return result; -} diff --git a/packages/forms/signals/test/web/form_field.spec.ts b/packages/forms/signals/test/web/form_field.spec.ts index 32fbd1dd590..b3ec07e11aa 100644 --- a/packages/forms/signals/test/web/form_field.spec.ts +++ b/packages/forms/signals/test/web/form_field.spec.ts @@ -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(fn: () => T): T { - try { - return fn(); - } finally { - TestBed.tick(); - } -} diff --git a/packages/forms/signals/test/web/interop.spec.ts b/packages/forms/signals/test/web/interop.spec.ts index e71261164e7..1673cd5862e 100644 --- a/packages/forms/signals/test/web/interop.spec.ts +++ b/packages/forms/signals/test/web/interop.spec.ts @@ -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(fn: () => T): T { - try { - return fn(); - } finally { - TestBed.tick(); - } -} - -async function actAsync(fn: () => T): Promise { - try { - return fn(); - } finally { - await TestBed.inject(ApplicationRef).whenStable(); - } -} diff --git a/packages/forms/signals/test/web/signal_form_control_web.spec.ts b/packages/forms/signals/test/web/signal_form_control_web.spec.ts index 9d4c93b3b10..0b047bb160a 100644 --- a/packages/forms/signals/test/web/signal_form_control_web.spec.ts +++ b/packages/forms/signals/test/web/signal_form_control_web.spec.ts @@ -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: ``, }) @@ -51,7 +50,6 @@ describe('SignalFormControl (web)', () => { it('binds inside nested FormGroup via formGroupName', () => { @Component({ - standalone: true, imports: [ReactiveFormsModule, FormField], template: `
@@ -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(fn: () => T): T { - try { - return fn(); - } finally { - TestBed.tick(); - } -} diff --git a/packages/private/testing/src/utils.ts b/packages/private/testing/src/utils.ts index 539ad439de5..e1e991c2c12 100644 --- a/packages/private/testing/src/utils.ts +++ b/packages/private/testing/src/utils.ts @@ -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( 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(fn: () => T): Promise { + 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(fn: () => T): T { + try { + return fn(); + } finally { + TestBed.tick(); + } +}