From 71b8159b37d2ed886b25ef4e8801cd2b168f17da Mon Sep 17 00:00:00 2001 From: Sonu Kapoor Date: Thu, 26 Feb 2026 10:31:41 -0500 Subject: [PATCH] test(forms): cover transformedValue without FormField context Adds a test verifying that `transformedValue` exposes parse errors via the returned signal's `parseErrors()` property when no FormField context is present. This ensures that: - parse errors are still observable without DI-based field propagation - the model is not updated when `parse` omits `value` - valid input clears parse errors and updates the model This test protects the documented contract that DI-based error propagation is expected for FormValueControl usage, while standalone usage relies on explicit consumption of `parseErrors()`. --- .../signals/test/node/parse_errors.spec.ts | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/packages/forms/signals/test/node/parse_errors.spec.ts b/packages/forms/signals/test/node/parse_errors.spec.ts index aa7c36941e4..1d6e9dde6b1 100644 --- a/packages/forms/signals/test/node/parse_errors.spec.ts +++ b/packages/forms/signals/test/node/parse_errors.spec.ts @@ -324,6 +324,59 @@ describe('parse errors', () => { await act(() => input.dispatchEvent(new Event('input'))); expect(comp.f().errors()).toEqual([jasmine.objectContaining({kind: 'max'})]); }); + + it('should expose parseErrors when transformedValue is used without FormField', async () => { + @Component({ + template: ` + + + @for (e of raw.parseErrors(); track $index) { +
{{ e.message }}
+ } + `, + }) + class TestCmp { + readonly value = model(5); + + protected readonly raw = transformedValue(this.value, { + parse: (rawValue: string) => { + if (rawValue === '') return {value: null}; + const num = Number(rawValue); + if (Number.isNaN(num)) { + return {error: {kind: 'parse', message: `${rawValue} is not numeric`}}; + } + return {value: num}; + }, + format: (val) => (val == null ? '' : String(val)), + }); + } + + const fixture = await act(() => TestBed.createComponent(TestCmp)); + const testEl = fixture.nativeElement as HTMLElement; + const comp = fixture.componentInstance; + + const input = testEl.querySelector('input') as HTMLInputElement; + + // Invalid input: model unchanged, parse error exposed. + input.value = 'abc'; + await act(() => input.dispatchEvent(new Event('input'))); + + expect(comp.value()).toBe(5); + + let errors = Array.from(testEl.querySelectorAll('.error')).map((e) => + (e.textContent ?? '').trim(), + ); + expect(errors).toEqual(['abc is not numeric']); + + // Valid input: model updated, parse errors cleared. + input.value = '42'; + await act(() => input.dispatchEvent(new Event('input'))); + + expect(comp.value()).toBe(42); + + errors = Array.from(testEl.querySelectorAll('.error')).map((e) => (e.textContent ?? '').trim()); + expect(errors).toEqual([]); + }); }); @Component({