What is happening
The second of the two NG0100 shapes the e2e suite reports. Observed on a real-backend run:
[change-detection] _GLAccountFormComponent, _FinancialActivityMappingFormComponent
on https://localhost:4200/tellers/1/cashiers/1/transactions
- NG0100: Previous value for 'ng-untouched': 'true'. Current value: 'false'.
at NgControlStatus_HostBindings
- NG0100: Previous value for 'ng-untouched': 'true'. Current value: 'false'.
at NgControlStatusGroup_HostBindings
The same shape appears for _LoginComponent on most tests, since every test signs in.
Unlike the two-way binding problem in the companion issue, it is not established that this one is an application defect at all, and that question should be answered before anyone writes a fix.
Why it might not be a defect
e2e/fixtures.ts already explains the mechanism:
The interval check samples at arbitrary moments, so it also catches states that are briefly inconsistent and then settle — reactive form validity is the one still outstanding […] enforcing today costs between zero and seventeen unrelated failures depending on timing.
app.config.ts runs checkNoChanges exhaustively on an interval. A control becomes touched when the user blurs it; if the interval fires between Angular's two passes during that interaction, the status legitimately differs between them and the error says so. That would make this an artifact of how the check samples, not a bug in the form.
Against that: a genuinely inconsistent state, even a brief one, is what [attr.aria-invalid] and the validation styling read. If a control's status can change without a change-detection pass, the error message a user sees can lag the control's real state.
Two corrections to what is currently recorded
1. It is not specific to reactive forms. The note in fixtures.ts attributes this to "reactive form validity". NgControlStatus's selector is [formControlName],[ngModel],[formControl], and gl-account-form.component.ts — one of the two components in the report above — uses FormsModule and [(ngModel)], not ReactiveFormsModule. Only 6 components in the application use reactive forms at all. Whatever the cause is, it reaches template-driven forms too.
2. The reported URL is wrong. The fixture accumulates errors for the whole test and prints page.url() when it flushes, so the URL is wherever the test finished, not where the error happened. That is why _GLAccountFormComponent appears above under a /tellers/… URL — an accounting form is not on that screen. Anyone chasing these from the log will start in the wrong place.
This is a small fix worth making first, since it makes the rest of the investigation possible: capture page.url() alongside each error as it arrives rather than once at the end.
What to do
- Fix the attribution in
e2e/fixtures.ts so each error carries the URL it occurred on.
- Determine the cause for one component —
gl-account-form is the smallest. Is the status changing during a render pass, or is the interval simply sampling mid-interaction? Disabling the interval check locally and reproducing by hand is the quickest discriminator.
- Then either fix the components, or — if it is a sampling artifact — narrow what the fixture treats as a failure, so that
ENFORCE_CD_ERRORS=1 can be turned on by default. Record the conclusion either way; the current note in fixtures.ts is a reasonable guess that has not been tested and is partly wrong.
Business value
The point is the check, not these particular messages. fixtures.ts says it plainly:
The value is in the reporting: this is what identified the six empty-dropdown components, all now fixed. Flip the default once the remaining sources are gone.
An advisory check that always prints something is one people stop reading. With the two-way bindings in the companion issue fixed and this question answered, ENFORCE_CD_ERRORS=1 becomes the default and the next change-detection regression fails a pull request instead of reaching a user.
Reproducing
npx playwright test --project=mocked e2e/functional-coverage.spec.ts 2>&1 | grep -A6 '\[change-detection\]'
ENFORCE_CD_ERRORS=1 npx playwright test --project=mocked e2e/functional-coverage.spec.ts
The backend project shows it too, but needs a Fineract stack; the mocked project does not.
Scope
In scope: the URL attribution fix, the diagnosis, and whichever of the two outcomes follows.
Out of scope: the 275 [(ngModel)] bindings — those are a separate, already-understood defect.
What is happening
The second of the two NG0100 shapes the e2e suite reports. Observed on a real-backend run:
The same shape appears for
_LoginComponenton most tests, since every test signs in.Unlike the two-way binding problem in the companion issue, it is not established that this one is an application defect at all, and that question should be answered before anyone writes a fix.
Why it might not be a defect
e2e/fixtures.tsalready explains the mechanism:app.config.tsrunscheckNoChangesexhaustively on an interval. A control becomes touched when the user blurs it; if the interval fires between Angular's two passes during that interaction, the status legitimately differs between them and the error says so. That would make this an artifact of how the check samples, not a bug in the form.Against that: a genuinely inconsistent state, even a brief one, is what
[attr.aria-invalid]and the validation styling read. If a control's status can change without a change-detection pass, the error message a user sees can lag the control's real state.Two corrections to what is currently recorded
1. It is not specific to reactive forms. The note in
fixtures.tsattributes this to "reactive form validity".NgControlStatus's selector is[formControlName],[ngModel],[formControl], andgl-account-form.component.ts— one of the two components in the report above — usesFormsModuleand[(ngModel)], notReactiveFormsModule. Only 6 components in the application use reactive forms at all. Whatever the cause is, it reaches template-driven forms too.2. The reported URL is wrong. The fixture accumulates errors for the whole test and prints
page.url()when it flushes, so the URL is wherever the test finished, not where the error happened. That is why_GLAccountFormComponentappears above under a/tellers/…URL — an accounting form is not on that screen. Anyone chasing these from the log will start in the wrong place.This is a small fix worth making first, since it makes the rest of the investigation possible: capture
page.url()alongside each error as it arrives rather than once at the end.What to do
e2e/fixtures.tsso each error carries the URL it occurred on.gl-account-formis the smallest. Is the status changing during a render pass, or is the interval simply sampling mid-interaction? Disabling the interval check locally and reproducing by hand is the quickest discriminator.ENFORCE_CD_ERRORS=1can be turned on by default. Record the conclusion either way; the current note infixtures.tsis a reasonable guess that has not been tested and is partly wrong.Business value
The point is the check, not these particular messages.
fixtures.tssays it plainly:An advisory check that always prints something is one people stop reading. With the two-way bindings in the companion issue fixed and this question answered,
ENFORCE_CD_ERRORS=1becomes the default and the next change-detection regression fails a pull request instead of reaching a user.Reproducing
The backend project shows it too, but needs a Fineract stack; the mocked project does not.
Scope
In scope: the URL attribution fix, the diagnosis, and whichever of the two outcomes follows.
Out of scope: the 275
[(ngModel)]bindings — those are a separate, already-understood defect.