mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
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:
committed by
Alex Rickabaugh
parent
13f3455a60
commit
cc05deab72
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user