mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
318fefad30
Moves common helper functions into a shared test utility to reduce duplication.
303 lines
9.3 KiB
TypeScript
303 lines
9.3 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright Google LLC All Rights Reserved.
|
|
*
|
|
* Use of this source code is governed by an MIT-style license that can be
|
|
* found in the LICENSE file at https://angular.dev/license
|
|
*/
|
|
|
|
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 () => {
|
|
@Component({
|
|
imports: [FormField],
|
|
template: `<input [formField]="f" />`,
|
|
})
|
|
class TestCmp {
|
|
readonly f = form(signal(''));
|
|
}
|
|
|
|
const fixture = act(() => TestBed.createComponent(TestCmp));
|
|
const input = fixture.nativeElement.firstChild;
|
|
|
|
expect(document.activeElement).not.toBe(input);
|
|
act(() => fixture.componentInstance.f().focusBoundControl());
|
|
expect(document.activeElement).toBe(input);
|
|
});
|
|
|
|
it('should delegate focus behavior to a custom control if focus logic is implemented', async () => {
|
|
let focusCalled = false;
|
|
|
|
@Component({
|
|
selector: 'custom-control',
|
|
host: {'tabindex': '-1'},
|
|
template: '<input #input />',
|
|
})
|
|
class CustomControl {
|
|
readonly value = model<string>();
|
|
readonly input = viewChild.required<ElementRef<HTMLInputElement>>('input');
|
|
focus() {
|
|
focusCalled = true;
|
|
this.input().nativeElement.focus();
|
|
}
|
|
}
|
|
|
|
@Component({
|
|
imports: [FormField, CustomControl],
|
|
template: `<custom-control [formField]="f" />`,
|
|
})
|
|
class TestCmp {
|
|
readonly f = form(signal(''));
|
|
}
|
|
|
|
const fixture = act(() => TestBed.createComponent(TestCmp));
|
|
const customControl = fixture.nativeElement.firstChild as HTMLInputElement;
|
|
|
|
act(() => fixture.componentInstance.f().focusBoundControl());
|
|
expect(focusCalled).toBeTrue();
|
|
expect(document.activeElement).not.toBe(customControl);
|
|
expect(document.activeElement).toBe(customControl.querySelector('input'));
|
|
});
|
|
|
|
it('should delegate focus behavior to a custom control composed with a FormField', async () => {
|
|
let focusCalled = false;
|
|
|
|
@Component({
|
|
selector: 'custom-control',
|
|
hostDirectives: [{directive: FormField, inputs: ['formField']}],
|
|
template: '<input #input />',
|
|
})
|
|
class CustomControl {
|
|
readonly value = model<string>();
|
|
readonly input = viewChild.required<ElementRef<HTMLInputElement>>('input');
|
|
focus() {
|
|
focusCalled = true;
|
|
this.input().nativeElement.focus();
|
|
}
|
|
}
|
|
|
|
@Component({
|
|
imports: [CustomControl],
|
|
template: `<custom-control [formField]="f" />`,
|
|
})
|
|
class TestCmp {
|
|
readonly f = form(signal(''));
|
|
}
|
|
|
|
const fixture = await act(() => TestBed.createComponent(TestCmp));
|
|
const customControl = fixture.nativeElement.firstChild as HTMLInputElement;
|
|
|
|
await act(() => fixture.componentInstance.f().focusBoundControl());
|
|
expect(focusCalled).toBeTrue();
|
|
expect(document.activeElement).not.toBe(customControl);
|
|
expect(document.activeElement).toBe(customControl.querySelector('input'));
|
|
});
|
|
|
|
it('should directly focus a custom control that has no custom focus logic', async () => {
|
|
@Component({
|
|
selector: 'custom-control',
|
|
host: {'tabindex': '-1'},
|
|
template: '',
|
|
})
|
|
class CustomControl {
|
|
readonly value = model<string>();
|
|
}
|
|
|
|
@Component({
|
|
imports: [FormField, CustomControl],
|
|
template: `<custom-control [formField]="f" />`,
|
|
})
|
|
class TestCmp {
|
|
readonly f = form(signal(''));
|
|
}
|
|
|
|
const fixture = act(() => TestBed.createComponent(TestCmp));
|
|
const customControl = fixture.nativeElement.firstChild as HTMLInputElement;
|
|
|
|
act(() => fixture.componentInstance.f().focusBoundControl());
|
|
expect(document.activeElement).toBe(customControl);
|
|
});
|
|
|
|
it('should focus the first (in the DOM) control bound to the field state', async () => {
|
|
@Component({
|
|
imports: [FormField],
|
|
template: `
|
|
@if (showFirst()) {
|
|
<input [formField]="f" id="input1" />
|
|
}
|
|
<input [formField]="f" id="input2" />
|
|
`,
|
|
})
|
|
class TestCmp {
|
|
readonly f = form(signal(''));
|
|
showFirst = signal(false);
|
|
}
|
|
|
|
const fixture = act(() => TestBed.createComponent(TestCmp));
|
|
const input2 = fixture.nativeElement.querySelector('#input2');
|
|
|
|
act(() => fixture.componentInstance.f().focusBoundControl());
|
|
expect(document.activeElement).toBe(input2);
|
|
|
|
act(() => fixture.componentInstance.showFirst.set(true));
|
|
const input1 = fixture.nativeElement.querySelector('#input1');
|
|
|
|
act(() => fixture.componentInstance.f().focusBoundControl());
|
|
expect(document.activeElement).toBe(input1);
|
|
});
|
|
|
|
it('should focus the first (in the DOM) bound child of a pass-through control', async () => {
|
|
@Component({
|
|
selector: 'custom-control',
|
|
imports: [FormField],
|
|
template: `
|
|
<input [formField]="formField().child2" id="child2" />
|
|
<input [formField]="formField().child1" id="child1" />
|
|
`,
|
|
})
|
|
class CustomControl {
|
|
formField = input.required<FieldTree<{child1: string; child2: string}>>();
|
|
}
|
|
|
|
@Component({
|
|
imports: [FormField, CustomControl],
|
|
template: `<custom-control [formField]="f" />`,
|
|
})
|
|
class TestCmp {
|
|
readonly f = form(signal({child1: '', child2: ''}));
|
|
}
|
|
|
|
const fixture = act(() => TestBed.createComponent(TestCmp));
|
|
const child2 = fixture.nativeElement.querySelector('#child2');
|
|
|
|
act(() => fixture.componentInstance.f().focusBoundControl());
|
|
expect(document.activeElement).toBe(child2);
|
|
});
|
|
|
|
it('should focus host element for a compat control', async () => {
|
|
@Component({
|
|
imports: [FormField],
|
|
template: `<input [formField]="f" />`,
|
|
})
|
|
class TestCmp {
|
|
readonly f = compatForm(signal(new FormControl('', {nonNullable: true})));
|
|
}
|
|
|
|
const fixture = act(() => TestBed.createComponent(TestCmp));
|
|
const input = fixture.nativeElement.firstChild;
|
|
|
|
expect(document.activeElement).not.toBe(input);
|
|
act(() => fixture.componentInstance.f().focusBoundControl());
|
|
expect(document.activeElement).toBe(input);
|
|
});
|
|
|
|
it('should not change focus if there is nothing to focus', async () => {
|
|
@Component({
|
|
selector: 'custom-control',
|
|
template: ``,
|
|
})
|
|
class CustomControl {
|
|
formField = input.required<Field<string>>();
|
|
}
|
|
|
|
@Component({
|
|
imports: [FormField, CustomControl],
|
|
template: `<custom-control [formField]="f" />`,
|
|
})
|
|
class TestCmp {
|
|
readonly f = form(signal(''));
|
|
}
|
|
|
|
const fixture = act(() => TestBed.createComponent(TestCmp));
|
|
|
|
const focusedEl = document.activeElement;
|
|
act(() => fixture.componentInstance.f().focusBoundControl());
|
|
expect(document.activeElement).toBe(focusedEl);
|
|
});
|
|
|
|
it('should focus a manually registered form field binding', async () => {
|
|
@Component({
|
|
selector: 'custom-control',
|
|
template: `<input #input />`,
|
|
})
|
|
class CustomControl {
|
|
formField = input.required<Field<string>>();
|
|
input = viewChild.required<ElementRef<HTMLInputElement>>('input');
|
|
|
|
constructor() {
|
|
inject(FormField, {self: true, optional: true})?.registerAsBinding({
|
|
focus: () => this.input().nativeElement.focus(),
|
|
});
|
|
}
|
|
}
|
|
|
|
@Component({
|
|
imports: [FormField, CustomControl],
|
|
template: `<custom-control [formField]="f" />`,
|
|
})
|
|
class TestCmp {
|
|
readonly f = form(signal(''));
|
|
}
|
|
|
|
const fixture = act(() => TestBed.createComponent(TestCmp));
|
|
const nativeInput = fixture.nativeElement.querySelector('custom-control > input');
|
|
expect(nativeInput).toBeTruthy();
|
|
|
|
act(() => fixture.componentInstance.f().focusBoundControl());
|
|
expect(document.activeElement).toBe(nativeInput);
|
|
});
|
|
|
|
it('should pass focus options to native control', async () => {
|
|
@Component({
|
|
imports: [FormField],
|
|
template: `<input [formField]="f" />`,
|
|
})
|
|
class TestCmp {
|
|
readonly f = form(signal(''));
|
|
}
|
|
|
|
const fixture = act(() => TestBed.createComponent(TestCmp));
|
|
const input = fixture.nativeElement.firstChild as HTMLInputElement;
|
|
|
|
const focusSpy = spyOn(input, 'focus');
|
|
|
|
act(() => fixture.componentInstance.f().focusBoundControl({preventScroll: true}));
|
|
expect(focusSpy).toHaveBeenCalledWith({preventScroll: true});
|
|
});
|
|
|
|
it('should pass focus options to custom control with focus method', async () => {
|
|
let receivedOptions: FocusOptions | undefined;
|
|
|
|
@Component({
|
|
selector: 'custom-control',
|
|
host: {'tabindex': '-1'},
|
|
template: '',
|
|
})
|
|
class CustomControl {
|
|
readonly value = model<string>();
|
|
focus(options?: FocusOptions) {
|
|
receivedOptions = options;
|
|
}
|
|
}
|
|
|
|
@Component({
|
|
imports: [FormField, CustomControl],
|
|
template: `<custom-control [formField]="f" />`,
|
|
})
|
|
class TestCmp {
|
|
readonly f = form(signal(''));
|
|
}
|
|
|
|
const fixture = act(() => TestBed.createComponent(TestCmp));
|
|
|
|
act(() => fixture.componentInstance.f().focusBoundControl({preventScroll: true}));
|
|
expect(receivedOptions).toEqual({preventScroll: true});
|
|
});
|
|
});
|