diff --git a/src/cdk/testing/testbed/fake-events/element-focus.ts b/src/cdk/testing/testbed/fake-events/element-focus.ts index 6b71c252f3c7..6a6d0232a1af 100644 --- a/src/cdk/testing/testbed/fake-events/element-focus.ts +++ b/src/cdk/testing/testbed/fake-events/element-focus.ts @@ -9,16 +9,31 @@ import {dispatchFakeEvent} from './dispatch-events'; function triggerFocusChange(element: HTMLElement, event: 'focus' | 'blur') { + const hasFocus = document.activeElement === element; + + if ((event === 'focus' && hasFocus) || (event === 'blur' && !hasFocus)) { + return; + } + let eventFired = false; const handler = () => (eventFired = true); element.addEventListener(event, handler); element[event](); element.removeEventListener(event, handler); + + // Some browsers won't move focus if the browser window is blurred while other will move it + // asynchronously. If that is the case, we fake the event sequence as a fallback. if (!eventFired) { - dispatchFakeEvent(element, event); + simulateFocusSequence(element, event); } } +/** Simulates the full event sequence for a focus event. */ +function simulateFocusSequence(element: HTMLElement, event: 'focus' | 'blur') { + dispatchFakeEvent(element, event); + dispatchFakeEvent(element, event === 'focus' ? 'focusin' : 'focusout'); +} + /** * Patches an elements focus and blur methods to emit events consistently and predictably. * This is necessary, because some browsers can call the focus handlers asynchronously, @@ -28,8 +43,8 @@ function triggerFocusChange(element: HTMLElement, event: 'focus' | 'blur') { // TODO: Check if this element focus patching is still needed for local testing, // where browser is not necessarily focused. export function patchElementFocus(element: HTMLElement) { - element.focus = () => dispatchFakeEvent(element, 'focus'); - element.blur = () => dispatchFakeEvent(element, 'blur'); + element.focus = () => simulateFocusSequence(element, 'focus'); + element.blur = () => simulateFocusSequence(element, 'blur'); } /** @docs-private */ diff --git a/src/components-examples/material/form-field/form-field-harness/form-field-harness-example.spec.ts b/src/components-examples/material/form-field/form-field-harness/form-field-harness-example.spec.ts index 77e14d92c1c9..54012bde1100 100644 --- a/src/components-examples/material/form-field/form-field-harness/form-field-harness-example.spec.ts +++ b/src/components-examples/material/form-field/form-field-harness/form-field-harness-example.spec.ts @@ -35,11 +35,13 @@ describe('FormFieldHarnessExample', () => { it('should be able to get error messages and hints of form-field', async () => { const formField = await loader.getHarness(MatFormFieldHarness); + const control = (await formField.getControl()) as MatInputHarness; expect(await formField.getTextErrors()).toEqual([]); expect(await formField.getTextHints()).toEqual(['Hint']); fixture.componentInstance.requiredControl.setValue(''); - await ((await formField.getControl()) as MatInputHarness)?.blur(); + await control.focus(); + await control.blur(); expect(await formField.getTextErrors()).toEqual(['Error']); expect(await formField.getTextHints()).toEqual([]); });