Repository: angular/angular
File: packages/forms/signals/test/web/field_directive.spec.ts (TypeScript)

Code before:
      expect(input.classList.contains('ng-dirty')).toBe(false);
      expect(input.classList.contains('ng-touched')).toBe(false);
      expect(input.classList.contains('ng-pending')).toBe(false);

      // Make it valid
      act(() => fixture.componentInstance.f().value.set('valid'));
      expect(input.classList.contains('ng-valid')).toBe(true);
      expect(input.classList.contains('ng-invalid')).toBe(false);

      // Touch it (simulating interaction)
      act(() => {
        input.dispatchEvent(new Event('blur'));
      });
      expect(input.classList.contains('ng-touched')).toBe(true);
      expect(input.classList.contains('ng-untouched')).toBe(false);

      // Make it dirty (simulating input)
      act(() => {
        input.value = 'new value';
        input.dispatchEvent(new Event('input'));
      });
      expect(input.classList.contains('ng-dirty')).toBe(true);
      expect(input.classList.contains('ng-pristine')).toBe(false);
    });

    it('should not apply classes on a custom and native control, but not a component with a `field` input', () => {
      TestBed.configureTestingModule({
        providers: [
          provideZonelessChangeDetection(),
          provideSignalFormsConfig({
            classes: {'always': () => true},
          }),
        ],
      });

      @Component({
        selector: 'custom-control',
        template: '',
      })
      class CustomControl implements FormValueControl<string> {
        readonly value = model.required<string>();
      }

      @Component({
        selector: 'custom-subform',
        template: '',
      })
      class CustomSubform {
        readonly field = input.required<FieldTree<string>>();
      }


Review comment (suggestion):
```suggestion
    it('should apply classes on a custom and native control, but not a component with a `field` input', () => {
```

Code after:
      @Component({
        imports: [Field],
        template: `<input [field]="f" />`,
      })
      class TestCmp {
        readonly f = form(signal(''), (p) => {
          debounce(p, () => promise);
        });
      }

      const fixture = act(() => TestBed.createComponent(TestCmp));
      const input = fixture.nativeElement.querySelector('input');

      act(() => {
        input.value = 'typing';
        input.dispatchEvent(new Event('input'));
      });
      expect(fixture.componentInstance.f().value()).toBe('');

      resolve();
      await promise;
      expect(fixture.componentInstance.f().value()).toBe('typing');
    });

    it('should support custom control', async () => {
      const {promise, resolve} = promiseWithResolvers<void>();

      @Component({
        selector: 'my-input',
        template: '<input #i [value]="value()" (input)="value.set(i.value)" />',
      })
      class CustomInput implements FormValueControl<string> {
        value = model('');
      }

      @Component({
        imports: [Field, CustomInput],
        template: `<my-input [field]="f" />`,
      })
      class TestCmp {
        readonly f = form(signal(''), (p) => {
          debounce(p, () => promise);
        });
        readonly customInput = viewChild.required(CustomInput);
      }

      const fixture = act(() => TestBed.createComponent(TestCmp));

      act(() => fixture.componentInstance.customInput().value.set('typing'));
      expect(fixture.componentInstance.f().value()).toBe('');