From 01bfb83fc9d84bdacbee2f3df095487d4afffc09 Mon Sep 17 00:00:00 2001 From: Leon Senft Date: Mon, 26 Jan 2026 12:39:42 -0800 Subject: [PATCH] test(forms): submit behavior while validation is pending Ensure `submit()` behaves as expected while a form is pending. - Submission is not blocked by pending validation. - Submission errors prevent pending validation errors from appearing after they resolve on the same field. - Submission errors don't prevent pending validation errors from appearing after they resolve on subfields. --- .../forms/signals/test/node/submit.spec.ts | 142 +++++++++++++++--- 1 file changed, 117 insertions(+), 25 deletions(-) diff --git a/packages/forms/signals/test/node/submit.spec.ts b/packages/forms/signals/test/node/submit.spec.ts index 568a05ca489..411bf5af2ca 100644 --- a/packages/forms/signals/test/node/submit.spec.ts +++ b/packages/forms/signals/test/node/submit.spec.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.dev/license */ -import {Injector, resource, signal} from '@angular/core'; +import {ApplicationRef, Injector, resource, signal} from '@angular/core'; import {TestBed} from '@angular/core/testing'; import { form, @@ -35,33 +35,125 @@ describe('submit', () => { expect(f.first().errors()).toEqual([requiredError({fieldTree: f.first})]); }); - it('should not block on pending async validators', async () => { - const data = signal(''); - const resolvers = promiseWithResolvers(); - const f = form( - data, - (p) => { - validateAsync(p, { - params: ({value}) => value(), - factory: (params) => - resource({ - params, - loader: () => resolvers.promise, - }), - onSuccess: () => {}, - onError: () => {}, - }); - }, - {injector: TestBed.inject(Injector)}, - ); + describe('while pending', () => { + it('should not block', async () => { + const data = signal(''); + const {promise} = promiseWithResolvers(); + const f = form( + data, + (p) => { + validateAsync(p, { + params: ({value}) => value(), + factory: (params) => + resource({ + params, + loader: () => promise, + }), + onSuccess: () => {}, + onError: () => {}, + }); + }, + {injector: TestBed.inject(Injector)}, + ); - expect(f().pending()).toBe(true); + expect(f().pending()).toBe(true); - const submitSpy = jasmine.createSpy(); - await submit(f, submitSpy); + const submitSpy = jasmine.createSpy(); + await submit(f, submitSpy); - expect(f().pending()).toBe(true); - expect(submitSpy).toHaveBeenCalled(); + expect(f().pending()).toBe(true); + expect(submitSpy).toHaveBeenCalled(); + }); + + it('should retain submit errors after pending validation resolves', async () => { + const appRef = TestBed.inject(ApplicationRef); + const data = signal('foo'); + const {promise, resolve} = promiseWithResolvers(); + const f = form( + data, + (p) => { + validateAsync(p, { + params: ({value}) => value(), + factory: (params) => + resource({ + params, + loader: () => promise, + }), + onSuccess: () => ({kind: 'async'}), + onError: (error) => fail(error), + }); + }, + {injector: TestBed.inject(Injector)}, + ); + + await submit(f, async () => ({kind: 'submit'})); + expect(f().errorSummary()).toEqual([jasmine.objectContaining({kind: 'submit'})]); + + resolve(true); + await appRef.whenStable(); + expect(f().errorSummary()).toEqual([jasmine.objectContaining({kind: 'submit'})]); + }); + + it('should resolve pending validation on subfield', async () => { + const appRef = TestBed.inject(ApplicationRef); + const data = signal({first: 'foo', last: 'bar'}); + const {promise, resolve} = promiseWithResolvers(); + const f = form( + data, + (p) => { + validateAsync(p.first, { + params: ({value}) => value(), + factory: (params) => + resource({ + params, + loader: () => promise, + }), + onSuccess: () => ({kind: 'async'}), + onError: (error) => fail(error), + }); + }, + {injector: TestBed.inject(Injector)}, + ); + + await submit(f, async () => ({kind: 'submit'})); + expect(f().errorSummary()).toEqual([jasmine.objectContaining({kind: 'submit'})]); + + resolve(true); + await appRef.whenStable(); + expect(f().errorSummary()).toEqual([ + jasmine.objectContaining({kind: 'submit', fieldTree: f}), + jasmine.objectContaining({kind: 'async', fieldTree: f.first}), + ]); + }); + + it('should resolve pending validation after successful submit', async () => { + const appRef = TestBed.inject(ApplicationRef); + const data = signal('foo'); + const {promise, resolve} = promiseWithResolvers(); + const f = form( + data, + (p) => { + validateAsync(p, { + params: ({value}) => value(), + factory: (params) => + resource({ + params, + loader: () => promise, + }), + onSuccess: () => ({kind: 'async'}), + onError: (error) => fail(error), + }); + }, + {injector: TestBed.inject(Injector)}, + ); + + await submit(f, async () => undefined); + expect(f().errorSummary()).toEqual([]); + + resolve(true); + await appRef.whenStable(); + expect(f().errorSummary()).toEqual([jasmine.objectContaining({kind: 'async'})]); + }); }); it('maps error to a field', async () => {