/**
 * @license
 * Copyright Google LLC All Rights Reserved.
 *
 * Use of this source code is governed by an MIT-style license that can be
 * found in the LICENSE file at https://angular.dev/license
 */

import {
  Directive,
  ElementRef,
  forwardRef,
  Host,
  Input,
  OnDestroy,
  Optional,
  Provider,
  Renderer2,
  ɵRuntimeError as RuntimeError,
} from '@angular/core';

import {RuntimeErrorCode} from '../errors';

import {
  BuiltInControlValueAccessor,
  ControlValueAccessor,
  NG_VALUE_ACCESSOR,
} from './control_value_accessor';

const SELECT_VALUE_ACCESSOR: Provider = {
  provide: NG_VALUE_ACCESSOR,
  useExisting: forwardRef(() => SelectControlValueAccessor),
  multi: true,
};

function _buildValueString(id: string | null, value: any): string {
  if (id == null) return `${value}`;
  if (value && typeof value === 'object') value = 'Object';
  return `${id}: ${value}`.slice(0, 50);
}

function _extractId(valueString: string): string {
  return valueString.split(':')[0];
}

/**
 * @description
 * The `ControlValueAccessor` for writing select control values and listening to select control
 * changes. The value accessor is used by the `FormControlDirective`, `FormControlName`, and
 * `NgModel` directives.
 *
 * @usageNotes
 *
 * ### Using select controls in a reactive form
 *
 * The following examples show how to use a select control in a reactive form.
 *
 * {@example forms/ts/reactiveSelectControl/reactive_select_control_example.ts region='Component'}
 *
 * ### Using select controls in a template-driven form
 *
 * To use a select in a template-driven form, simply add an `ngModel` and a `name`
 * attribute to the main `<select>` tag.
 *
 * {@example forms/ts/selectControl/select_control_example.ts region='Component'}
 *
 * ### Customizing option selection
 *
 * Angular uses object identity to select option. It's possible for the identities of items
 * to change while the data does not. This can happen, for example, if the items are produced
 * from an RPC to the server, and that RPC is re-run. Even if the data hasn't changed, the
 * second response will produce objects with different identities.
 *
 * To customize the default option comparison algorithm, `<select>` supports `compareWith` input.
 * `compareWith` takes a **function** which has two arguments: `option1` and `option2`.
 * If `compareWith` is given, Angular selects option by the return value of the function.
 *
 * ```ts
 * const selectedCountriesControl = new FormControl();
 * ```
 *
 * ```
 * <select [compareWith]="compareFn"  [formControl]="selectedCountriesControl">
 *     <option *ngFor="let country of countries" [ngValue]="country">
 *         {{country.name}}
 *     </option>
 * </select>
 *
 * compareFn(c1: Country, c2: Country): boolean {
 *     return c1 && c2 ? c1.id === c2.id : c1 === c2;
 * }
 * ```
 *
 * **Note:** We listen to the 'change' event because 'input' events aren't fired
 * for selects in IE, see:
 * https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/input_event#browser_compatibility
 *
 * @ngModule ReactiveFormsModule
 * @ngModule FormsModule
 * @publicApi
 */
@Directive({
  selector:
    'select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]',
  host: {'(change)': 'onChange($event.target.value)', '(blur)': 'onTouched()'},
  providers: [SELECT_VALUE_ACCESSOR],
  standalone: false,
})
export class SelectControlValueAccessor
  extends BuiltInControlValueAccessor
  implements ControlValueAccessor
{
  /** @nodoc */
  value: any;

  /** @internal */
  _optionMap: Map<string, any> = new Map<string, any>();

  /** @internal */
  _idCounter: number = 0;

  /**
   * @description
   * Tracks the option comparison algorithm for tracking identities when
   * checking for changes.
   */
  @Input()
  set compareWith(fn: (o1: any, o2: any) => boolean) {
    if (typeof fn !== 'function' && (typeof ngDevMode === 'undefined' || ngDevMode)) {
      throw new RuntimeError(
        RuntimeErrorCode.COMPAREWITH_NOT_A_FN,
        `compareWith must be a function, but received ${JSON.stringify(fn)}`,
      );
    }
    this._compareWith = fn;
  }

  private _compareWith: (o1: any, o2: any) => boolean = Object.is;

  /**
   * Sets the "value" property on the select element.
   * @nodoc
   */
  writeValue(value: any): void {
    this.value = value;
    const id: string | null = this._getOptionId(value);
    const valueString = _buildValueString(id, value);
    this.setProperty('value', valueString);
  }

  /**
   * Registers a function called when the control value changes.
   * @nodoc
   */
  override registerOnChange(fn: (value: any) => any): void {
    this.onChange = (valueString: string) => {
      this.value = this._getOptionValue(valueString);
      fn(this.value);
    };
  }

  /** @internal */
  _registerOption(): string {
    return (this._idCounter++).toString();
  }

  /** @internal */
  _getOptionId(value: any): string | null {
    for (const id of this._optionMap.keys()) {
      if (this._compareWith(this._optionMap.get(id), value)) return id;
    }
    return null;
  }

  /** @internal */
  _getOptionValue(valueString: string): any {
    const id: string = _extractId(valueString);
    return this._optionMap.has(id) ? this._optionMap.get(id) : valueString;
  }
}

/**
 * @description
 * Marks `<option>` as dynamic, so Angular can be notified when options change.
 *
 * @see {@link SelectControlValueAccessor}
 *
 * @ngModule ReactiveFormsModule
 * @ngModule FormsModule
 * @publicApi
 */
@Directive({
  selector: 'option',
  standalone: false,
})
export class NgSelectOption implements OnDestroy {
  /**
   * @description
   * ID of the option element
   */
  // TODO(issue/24571): remove '!'.
  id!: string;

  constructor(
    private _element: ElementRef,
    private _renderer: Renderer2,
    @Optional() @Host() private _select: SelectControlValueAccessor,
  ) {
    if (this._select) this.id = this._select._registerOption();
  }

  /**
   * @description
   * Tracks the value bound to the option element. Unlike the value binding,
   * ngValue supports binding to objects.
   */
  @Input('ngValue')
  set ngValue(value: any) {
    if (this._select == null) return;
    this._select._optionMap.set(this.id, value);
    this._setElementValue(_buildValueString(this.id, value));
    this._select.writeValue(this._select.value);
  }

  /**
   * @description
   * Tracks simple string values bound to the option element.
   * For objects, use the `ngValue` input binding.
   */
  @Input('value')
  set value(value: any) {
    this._setElementValue(value);
    if (this._select) this._select.writeValue(this._select.value);
  }

  /** @internal */
  _setElementValue(value: string): void {
    this._renderer.setProperty(this._element.nativeElement, 'value', value);
  }

  /** @nodoc */
  ngOnDestroy(): void {
    if (this._select) {
      this._select._optionMap.delete(this.id);
      this._select.writeValue(this._select.value);
    }
  }
}


/**
 * @license
 * Copyright Google LLC All Rights Reserved.
 *
 * Use of this source code is governed by an MIT-style license that can be
 * found in the LICENSE file at https://angular.dev/license
 */

export const formControlNameExample = `

    <input formControlName="firstName">

  In your class:

  this.myGroup = new FormGroup({
      firstName: new FormControl()
  });`;

export const formGroupNameExample = `

        <input formControlName="firstName">

  In your class:

  this.myGroup = new FormGroup({
      person: new FormGroup({ firstName: new FormControl() })
  });`;

export const formArrayNameExample = `

        <input [formControlName]="i">

  In your class:

  this.cityArray = new FormArray([new FormControl('SF')]);
  this.myGroup = new FormGroup({
    cities: this.cityArray
  });`;

export const ngModelGroupExample = `
  <form>

        <input [(ngModel)]="person.name" name="firstName">

  </form>`;

export const ngModelWithFormGroupExample = `

      <input formControlName="firstName">
      <input [(ngModel)]="showMoreControls" [ngModelOptions]="{standalone: true}">

`;


/**
 * @license
 * Copyright Google LLC All Rights Reserved.
 *
 * Use of this source code is governed by an MIT-style license that can be
 * found in the LICENSE file at https://angular.dev/license
 */

import {
  Directive,
  forwardRef,
  Host,
  Inject,
  Input,
  OnDestroy,
  OnInit,
  Optional,
  Provider,
  Self,
  SkipSelf,
} from '@angular/core';

import {FormArray} from '../../model/form_array';
import {NG_ASYNC_VALIDATORS, NG_VALIDATORS} from '../../validators';
import {AbstractFormGroupDirective} from '../abstract_form_group_directive';
import {ControlContainer} from '../control_container';
import {arrayParentException, groupParentException} from '../reactive_errors';
import {controlPath} from '../shared';
import {AsyncValidator, AsyncValidatorFn, Validator, ValidatorFn} from '../validators';

import {FormGroupDirective} from './form_group_directive';

const formGroupNameProvider: Provider = {
  provide: ControlContainer,
  useExisting: forwardRef(() => FormGroupName),
};

/**
 * @description
 *
 * Syncs a nested `FormGroup` or `FormRecord` to a DOM element.
 *
 * This directive can only be used with a parent `FormGroupDirective`.
 *
 * It accepts the string name of the nested `FormGroup` or `FormRecord` to link, and
 * looks for a `FormGroup` or `FormRecord` registered with that name in the parent
 * `FormGroup` instance you passed into `FormGroupDirective`.
 *
 * Use nested form groups to validate a sub-group of a
 * form separately from the rest or to group the values of certain
 * controls into their own nested object.
 *
 * @see [Reactive Forms Guide](guide/forms/reactive-forms)
 *
 * @usageNotes
 *
 * ### Access the group by name
 *
 * The following example uses the `AbstractControl.get` method to access the
 * associated `FormGroup`
 *
 * ```ts
 *   this.form.get('name');
 * ```
 *
 * ### Access individual controls in the group
 *
 * The following example uses the `AbstractControl.get` method to access
 * individual controls within the group using dot syntax.
 *
 * ```ts
 *   this.form.get('name.first');
 * ```
 *
 * ### Register a nested `FormGroup`.
 *
 * The following example registers a nested *name* `FormGroup` within an existing `FormGroup`,
 * and provides methods to retrieve the nested `FormGroup` and individual controls.
 *
 * {@example forms/ts/nestedFormGroup/nested_form_group_example.ts region='Component'}
 *
 * @ngModule ReactiveFormsModule
 * @publicApi
 */
@Directive({
  selector: '[formGroupName]',
  providers: [formGroupNameProvider],
  standalone: false,
})
export class FormGroupName extends AbstractFormGroupDirective implements OnInit, OnDestroy {
  /**
   * @description
   * Tracks the name of the `FormGroup` bound to the directive. The name corresponds
   * to a key in the parent `FormGroup` or `FormArray`.
   * Accepts a name as a string or a number.
   * The name in the form of a string is useful for individual forms,
   * while the numerical form allows for form groups to be bound
   * to indices when iterating over groups in a `FormArray`.
   */
  @Input('formGroupName') override name: string | number | null = null;

  constructor(
    @Optional() @Host() @SkipSelf() parent: ControlContainer,
    @Optional() @Self() @Inject(NG_VALIDATORS) validators: (Validator | ValidatorFn)[],
    @Optional()
    @Self()
    @Inject(NG_ASYNC_VALIDATORS)
    asyncValidators: (AsyncValidator | AsyncValidatorFn)[],
  ) {
    super();
    this._parent = parent;
    this._setValidators(validators);
    this._setAsyncValidators(asyncValidators);
  }

  /** @internal */
  override _checkParentType(): void {
    if (_hasInvalidParent(this._parent) && (typeof ngDevMode === 'undefined' || ngDevMode)) {
      throw groupParentException();
    }
  }
}

export const formArrayNameProvider: any = {
  provide: ControlContainer,
  useExisting: forwardRef(() => FormArrayName),
};

/**
 * @description
 *
 * Syncs a nested `FormArray` to a DOM element.
 *
 * This directive is designed to be used with a parent `FormGroupDirective` (selector:
 * `[formGroup]`).
 *
 * It accepts the string name of the nested `FormArray` you want to link, and
 * will look for a `FormArray` registered with that name in the parent
 * `FormGroup` instance you passed into `FormGroupDirective`.
 *
 * @see [Reactive Forms Guide](guide/forms/reactive-forms)
 * @see {@link AbstractControl}
 *
 * @usageNotes
 *
 * ### Example
 *
 * {@example forms/ts/nestedFormArray/nested_form_array_example.ts region='Component'}
 *
 * @ngModule ReactiveFormsModule
 * @publicApi
 */
@Directive({
  selector: '[formArrayName]',
  providers: [formArrayNameProvider],
  standalone: false,
})
export class FormArrayName extends ControlContainer implements OnInit, OnDestroy {
  /** @internal */
  _parent: ControlContainer;

  /**
   * @description
   * Tracks the name of the `FormArray` bound to the directive. The name corresponds
   * to a key in the parent `FormGroup` or `FormArray`.
   * Accepts a name as a string or a number.
   * The name in the form of a string is useful for individual forms,
   * while the numerical form allows for form arrays to be bound
   * to indices when iterating over arrays in a `FormArray`.
   */
  @Input('formArrayName') override name: string | number | null = null;

  constructor(
    @Optional() @Host() @SkipSelf() parent: ControlContainer,
    @Optional() @Self() @Inject(NG_VALIDATORS) validators: (Validator | ValidatorFn)[],
    @Optional()
    @Self()
    @Inject(NG_ASYNC_VALIDATORS)
    asyncValidators: (AsyncValidator | AsyncValidatorFn)[],
  ) {
    super();
    this._parent = parent;
    this._setValidators(validators);
    this._setAsyncValidators(asyncValidators);
  }

  /**
   * A lifecycle method called when the directive's inputs are initialized. For internal use only.
   * @throws If the directive does not have a valid parent.
   * @nodoc
   */
  ngOnInit(): void {
    this._checkParentType();
    this.formDirective!.addFormArray(this);
  }

  /**
   * A lifecycle method called before the directive's instance is destroyed. For internal use only.
   * @nodoc
   */
  ngOnDestroy(): void {
    if (this.formDirective) {
      this.formDirective.removeFormArray(this);
    }
  }

  /**
   * @description
   * The `FormArray` bound to this directive.
   */
  override get control(): FormArray {
    return this.formDirective!.getFormArray(this);
  }

  /**
   * @description
   * The top-level directive for this group if present, otherwise null.
   */
  override get formDirective(): FormGroupDirective | null {
    return this._parent ? <FormGroupDirective>this._parent.formDirective : null;
  }

  /**
   * @description
   * Returns an array that represents the path from the top-level form to this control.
   * Each index is the string name of the control on that level.
   */
  override get path(): string[] {
    return controlPath(this.name == null ? this.name : this.name.toString(), this._parent);
  }

  private _checkParentType(): void {
    if (_hasInvalidParent(this._parent) && (typeof ngDevMode === 'undefined' || ngDevMode)) {
      throw arrayParentException();
    }
  }
}

function _hasInvalidParent(parent: ControlContainer): boolean {
  return (
    !(parent instanceof FormGroupName) &&
    !(parent instanceof FormGroupDirective) &&
    !(parent instanceof FormArrayName)
  );
}


/**
 * @license
 * Copyright Google LLC All Rights Reserved.
 *
 * Use of this source code is governed by an MIT-style license that can be
 * found in the LICENSE file at https://angular.dev/license
 */

import {ɵWritable as Writable} from '@angular/core';

import {AsyncValidatorFn, ValidatorFn} from '../directives/validators';

import {
  AbstractControl,
  AbstractControlOptions,
  assertAllValuesPresent,
  assertControlPresent,
  pickAsyncValidators,
  pickValidators,
  ɵRawValue,
  ɵTypedOrUntyped,
  ɵValue,
} from './abstract_model';

/**
 * FormGroupValue extracts the type of `.value` from a FormGroup's inner object type. The untyped
 * case falls back to {[key: string]: any}.
 *
 * Angular uses this type internally to support Typed Forms; do not use it directly.
 *
 * For internal use only.
 */
export type ɵFormGroupValue<T extends {[K in keyof T]?: AbstractControl<any>}> = ɵTypedOrUntyped<
  T,
  Partial<{[K in keyof T]: ɵValue<T[K]>}>,
  {[key: string]: any}
>;

/**
 * FormGroupRawValue extracts the type of `.getRawValue()` from a FormGroup's inner object type. The
 * untyped case falls back to {[key: string]: any}.
 *
 * Angular uses this type internally to support Typed Forms; do not use it directly.
 *
 * For internal use only.
 */
export type ɵFormGroupRawValue<T extends {[K in keyof T]?: AbstractControl<any>}> = ɵTypedOrUntyped<
  T,
  {[K in keyof T]: ɵRawValue<T[K]>},
  {[key: string]: any}
>;

/**
 * OptionalKeys returns the union of all optional keys in the object.
 *
 * Angular uses this type internally to support Typed Forms; do not use it directly.
 */
export type ɵOptionalKeys<T> = {
  [K in keyof T]-?: undefined extends T[K] ? K : never;
}[keyof T];

/**
 * Tracks the value and validity state of a group of `FormControl` instances.
 *
 * A `FormGroup` aggregates the values of each child `FormControl` into one object,
 * with each control name as the key.  It calculates its status by reducing the status values
 * of its children. For example, if one of the controls in a group is invalid, the entire
 * group becomes invalid.
 *
 * `FormGroup` is one of the four fundamental building blocks used to define forms in Angular,
 * along with `FormControl`, `FormArray`, and `FormRecord`.
 *
 * When instantiating a `FormGroup`, pass in a collection of child controls as the first
 * argument. The key for each child registers the name for the control.
 *
 * `FormGroup` is intended for use cases where the keys are known ahead of time.
 * If you need to dynamically add and remove controls, use {@link FormRecord} instead.
 *
 * `FormGroup` accepts an optional type parameter `TControl`, which is an object type with inner
 * control types as values.
 *
 * @usageNotes
 *
 * ### Create a form group with 2 controls
 *
 * ```
 * const form = new FormGroup({
 *   first: new FormControl('Nancy', Validators.minLength(2)),
 *   last: new FormControl('Drew'),
 * });
 *
 * console.log(form.value);   // {first: 'Nancy', last; 'Drew'}
 * console.log(form.status);  // 'VALID'
 * ```
 *
 * ### The type argument, and optional controls
 *
 * `FormGroup` accepts one generic argument, which is an object containing its inner controls.
 * This type will usually be inferred automatically, but you can always specify it explicitly if you
 * wish.
 *
 * If you have controls that are optional (i.e. they can be removed, you can use the `?` in the
 * type):
 *
 * ```
 * const form = new FormGroup<{
 *   first: FormControl<string|null>,
 *   middle?: FormControl<string|null>, // Middle name is optional.
 *   last: FormControl<string|null>,
 * }>({
 *   first: new FormControl('Nancy'),
 *   last: new FormControl('Drew'),
 * });
 * ```
 *
 * ### Create a form group with a group-level validator
 *
 * You include group-level validators as the second arg, or group-level async
 * validators as the third arg. These come in handy when you want to perform validation
 * that considers the value of more than one child control.
 *
 * ```
 * const form = new FormGroup({
 *   password: new FormControl('', Validators.minLength(2)),
 *   passwordConfirm: new FormControl('', Validators.minLength(2)),
 * }, passwordMatchValidator);
 *
 *
 * function passwordMatchValidator(g: FormGroup) {
 *    return g.get('password').value === g.get('passwordConfirm').value
 *       ? null : {'mismatch': true};
 * }
 * ```
 *
 * Like `FormControl` instances, you choose to pass in
 * validators and async validators as part of an options object.
 *
 * ```
 * const form = new FormGroup({
 *   password: new FormControl('')
 *   passwordConfirm: new FormControl('')
 * }, { validators: passwordMatchValidator, asyncValidators: otherValidator });
 * ```
 *
 * ### Set the updateOn property for all controls in a form group
 *
 * The options object is used to set a default value for each child
 * control's `updateOn` property. If you set `updateOn` to `'blur'` at the
 * group level, all child controls default to 'blur', unless the child
 * has explicitly specified a different `updateOn` value.
 *
 * ```ts
 * const c = new FormGroup({
 *   one: new FormControl()
 * }, { updateOn: 'blur' });
 * ```
 *
 * ### Using a FormGroup with optional controls
 *
 * It is possible to have optional controls in a FormGroup. An optional control can be removed later
 * using `removeControl`, and can be omitted when calling `reset`. Optional controls must be
 * declared optional in the group's type.
 *
 * ```ts
 * const c = new FormGroup<{one?: FormControl<string>}>({
 *   one: new FormControl('')
 * });
 * ```
 *
 * Notice that `c.value.one` has type `string|null|undefined`. This is because calling `c.reset({})`
 * without providing the optional key `one` will cause it to become `null`.
 *
 * @publicApi
 */
export cla


s FormGroup<
  TControl extends {[K in keyof TControl]: AbstractControl<any>} = any,
> extends AbstractControl<
  ɵTypedOrUntyped<TControl, ɵFormGroupValue<TControl>, any>,
  ɵTypedOrUntyped<TControl, ɵFormGroupRawValue<TControl>, any>
> {
  /**
   * Creates a new `FormGroup` instance.
   *
   * @param controls A collection of child controls. The key for each child is the name
   * under which it is registered.
   *
   * @param validatorOrOpts A synchronous validator function, or an array of
   * such functions, or an `AbstractControlOptions` object that contains validation functions
   * and a validation trigger.
   *
   * @param asyncValidator A single async validator or array of async validator functions
   *
   */
  constructor(
    controls: TControl,
    validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null,
    asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null,
  ) {
    super(pickValidators(validatorOrOpts), pickAsyncValidators(asyncValidator, validatorOrOpts));
    (typeof ngDevMode === 'undefined' || ngDevMode) && validateFormGroupControls(controls);
    this.controls = controls;
    this._initObservables();
    this._setUpdateStrategy(validatorOrOpts);
    this._setUpControls();
    this.updateValueAndValidity({
      onlySelf: true,
      // If `asyncValidator` is present, it will trigger control status change from `PENDING` to
      // `VALID` or `INVALID`. The status should be broadcasted via the `statusChanges` observable,
      // so we set `emitEvent` to `true` to allow that during the control creation process.
      emitEvent: !!this.asyncValidator,
    });
  }

  public controls: ɵTypedOrUntyped<TControl, TControl, {[key: string]: AbstractControl<any>}>;

  /**
   * Registers a control with the group's list of controls. In a strongly-typed group, the control
   * must be in the group's type (possibly as an optional key).
   *
   * This method does not update the value or validity of the control.
   * Use {@link FormGroup#addControl addControl} instead.
   *
   * @param name The control name to register in the collection
   * @param control Provides the control for the given name
   */
  registerControl<K extends string & keyof TControl>(name: K, control: TControl[K]): TControl[K];
  registerControl(
    this: FormGroup<{[key: string]: AbstractControl<any>}>,
    name: string,
    control: AbstractControl<any>,
  ): AbstractControl<any>;

  registerControl<K extends string & keyof TControl>(name: K, control: TControl[K]): TControl[K] {
    if (this.controls[name]) return (this.controls as any)[name];
    this.controls[name] = control;
    control.setParent(this as FormGroup);
    control._registerOnCollectionChange(this._onCollectionChange);
    return control;
  }

  /**
   * Add a control to this group. In a strongly-typed group, the control must be in the group's type
   * (possibly as an optional key).
   *
   * If a control with a given name already exists, it would *not* be replaced with a new one.
   * If you want to replace an existing control, use the {@link FormGroup#setControl setControl}
   * method instead. This method also updates the value and validity of the control.
   *
   * @param name The control name to add to the collection
   * @param control Provides the control for the given name
   * @param options Specifies whether this FormGroup instance should emit events after a new
   *     control is added.
   * * `emitEvent`: When true or not supplied (the default), both the `statusChanges` and
   * `valueChanges` observables emit events with the latest status and value when the control is
   * added. When false, no events are emitted.
   */
  addControl(
    this: FormGroup<{[key: string]: AbstractControl<any>}>,
    name: string,
    control: AbstractControl,
    options?: {emitEvent?: boolean},
  ): void;
  addControl<K extends string & keyof TControl>(
    name: K,
    control: Required<TControl>[K],
    options?: {
      emitEvent?: boolean;
    },
  ): void;

  addControl<K extends string & keyof TControl>(
    name: K,
    control: Required<TControl>[K],
    options: {
      emitEvent?: boolean;
    } = {},
  ): void {
    this.registerControl(name, control);
    this.updateValueAndValidity({emitEvent: options.emitEvent});
    this._onCollectionChange();
  }

  removeControl(
    this: FormGroup<{[key: string]: AbstractControl<any>}>,
    name: string,
    options?: {
      emitEvent?: boolean;
    },
  ): void;
  removeControl<S extends string>(
    name: ɵOptionalKeys<TControl> & S,
    options?: {
      emitEvent?: boolean;
    },
  ): void;

  /**
   * Remove a control from this group. In a strongly-typed group, required controls cannot be
   * removed.
   *
   * This method also updates the value and validity of the control.
   *
   * @param name The control name to remove from the collection
   * @param options Specifies whether this FormGroup instance should emit events after a
   *     control is removed.
   * * `emitEvent`: When true or not supplied (the default), both the `statusChanges` and
   * `valueChanges` observables emit events with the latest status and value when the control is
   * removed. When false, no events are emitted.
   */
  removeControl(name: string, options: {emitEvent?: boolean} = {}): void {
    if ((this.controls as any)[name])
      (this.controls as any)[name]._registerOnCollectionChange(() => {});
    delete (this.controls as any)[name];
    this.updateValueAndValidity({emitEvent: options.emitEvent});
    this._onCollectionChange();
  }

  /**
   * Replace an existing control. In a strongly-typed group, the control must be in the group's type
   * (possibly as an optional key).
   *
   * If a control with a given name does not exist in this `FormGroup`, it will be added.
   *
   * @param name The control name to replace in the collection
   * @param control Provides the control for the given name
   * @param options Specifies whether this FormGroup instance should emit events after an
   *     existing control is replaced.
   * * `emitEvent`: When true or not supplied (the default), both the `statusChanges` and
   * `valueChanges` observables emit events with the latest status and value when the control is
   * replaced with a new one. When false, no events are emitted.
   */
  setControl<K extends string & keyof TControl>(
    name: K,
    control: TControl[K],
    options?: {
      emitEvent?: boolean;
    },
  ): void;
  setControl(
    this: FormGroup<{[key: string]: AbstractControl<any>}>,
    name: string,
    control: AbstractControl,
    options?: {emitEvent?: boolean},
  ): void;

  setControl<K extends string & keyof TControl>(
    name: K,
    control: TControl[K],
    options: {
      emitEvent?: boolean;
    } = {},
  ): void {
    if (this.controls[name]) this.controls[name]._registerOnCollectionChange(() => {});
    delete this.controls[name];
    if (control) this.registerControl(name, control);
    this.updateValueAndValidity({emitEvent: options.emitEvent});
    this._onCollectionChange();
  }

  /**
   * Check whether there is an enabled control with the given name in the group.
   *
   * Reports false for disabled controls. If you'd like to check for existence in the group
   * only, use {@link AbstractControl#get get} instead.
   *
   * @param controlName The control name to check for existence in the collection
   *
   * @returns false for disabled controls, true otherwise.
   */
  contains<K extends string>(controlName: K): boolean;
  contains(this: FormGroup<{[key: string]: AbstractControl<any>}>, controlName: string): boolean;

  contains<K extends string & keyof TControl>(controlName: K): boolean {
    return this.controls.hasOwnProperty(controlName) && this.controls[controlName].enabled;
  }

  /**
   * Sets


observable that emits an event every time the state of the control changes.
   * It emits for value, status, pristine or touched changes.
   *
   * **Note**: On value change, the emit happens right after a value of this control is updated. The
   * value of a parent control (for example if this FormControl is a part of a FormGroup) is updated
   * later, so accessing a value of a parent control (using the `value` property) from the callback
   * of this event might result in getting a value that has not been updated yet. Subscribe to the
   * `events` of the parent control instead.
   * For other event types, the events are emitted after the parent control has been updated.
   *
   */
  public readonly events = this._events.asObservable();

  /**
   * A multicasting observable that emits an event every time the value of the control changes, in
   * the UI or programmatically. It also emits an event each time you call enable() or disable()
   * without passing along {emitEvent: false} as a function argument.
   *
   * **Note**: the emit happens right after a value of this control is updated. The value of a
   * parent control (for example if this FormControl is a part of a FormGroup) is updated later, so
   * accessing a value of a parent control (using the `value` property) from the callback of this
   * event might result in getting a value that has not been updated yet. Subscribe to the
   * `valueChanges` event of the parent control instead.
   *
   * TODO: this should be piped from events() but is breaking in G3
   */
  public readonly valueChanges!: Observable<TValue>;

  /**
   * A multicasting observable that emits an event every time the validation `status` of the control
   * recalculates.
   *
   * @see {@link FormControlStatus}
   * @see {@link AbstractControl.status}
   *
   * TODO: this should be piped from events() but is breaking in G3
   */
  public readonly statusChanges!: Observable<FormControlStatus>;

  /**
   * Reports the update strategy of the `AbstractControl` (meaning
   * the event on which the control updates itself).
   * Possible values: `'change'` | `'blur'` | `'submit'`
   * Default value: `'change'`
   */
  get updateOn(): FormHooks {
    return this._updateOn ? this._updateOn : this.parent ? this.parent.updateOn : 'change';
  }

  /**
   * Sets the synchronous validators that are active on this control.  Calling
   * this overwrites any existing synchronous validators.
   *
   * When you add or remove a validator at run time, you must call
   * `updateValueAndValidity()` for the new validation to take effect.
   *
   * If you want to add a new validator without affecting existing ones, consider
   * using `addValidators()` method instead.
   */
  setValidators(validators: ValidatorFn | ValidatorFn[] | null): void {
    this._assignValidators(validators);
  }

  /**
   * Sets the asynchronous validators that are active on this control. Calling this
   * overwrites any existing asynchronous validators.
   *
   * When you add or remove a validator at run time, you must call
   * `updateValueAndValidity()` for the new validation to take effect.
   *
   * If you want to add a new validator without affecting existing ones, consider
   * using `addAsyncValidators()` method instead.
   */
  setAsyncValidators(validators: AsyncValidatorFn | AsyncValidatorFn[] | null): void {
    this._assignAsyncValidators(validators);
  }

  /**
   * Add a synchronous validator or validators to this control, without affecting other validators.
   *
   * When you add or remove a validator at run time, you must call
   * `updateValueAndValidity()` for the new validation to take effect.
   *
   * Adding a validator that already exists will have no effect. If duplicate validator functions
   * are present in the `validators` array, only the first instance would be added to a form
   * control.
   *
   * @param validators The new validator function or functions to add to this control.
   */
  addValidators(validators: ValidatorFn | ValidatorFn[]): void {
    this.setValidators(addValidators(validators, this._rawValidators));
  }

  /**
   * Add an asynchronous validator or validators to this control, without affecting other
   * validators.
   *
   * When you add or remove a validator at run time, you must call
   * `updateValueAndValidity()` for the new validation to take effect.
   *
   * Adding a validator that already exists will have no effect.
   *
   * @param validators The new asynchronous validator function or functions to add to this control.
   */
  addAsyncValidators(validators: AsyncValidatorFn | AsyncValidatorFn[]): void {
    this.setAsyncValidators(addValidators(validators, this._rawAsyncValidators));
  }

  /**
   * Remove a synchronous validator from this control, without affecting other validators.
   * Validators are compared by function reference; you must pass a reference to the exact same
   * validator function as the one that was originally set. If a provided validator is not found,
   * it is ignored.
   *
   * @usageNotes
   *
   * ### Reference to a ValidatorFn
   *
   * ```
   * // Reference to the RequiredValidator
   * const ctrl = new FormControl<string | null>('', Validators.required);
   * ctrl.removeValidators(Validators.required);
   *
   * // Reference to anonymous function inside MinValidator
   * const minValidator = Validators.min(3);
   * const ctrl = new FormControl<string | null>('', minValidator);
   * expect(ctrl.hasValidator(minValidator)).toEqual(true)
   * expect(ctrl.hasValidator(Validators.min(3))).toEqual(false)
   *
   * ctrl.removeValidators(minValidator);
   * ```
   *
   * When you add or remove a validator at run time, you must call
   * `updateValueAndValidity()` for the new validation to take effect.
   *
   * @param validators The validator or validators to remove.
   */
  removeValidators(validators: ValidatorFn | ValidatorFn[]): void {
    this.setValidators(removeValidators(validators, this._rawValidators));
  }

  /**
   * Remove an asynchronous validator from this control, without affecting other validators.
   * Validators are compared by function reference; you must pass a reference to the exact same
   * validator function as the one that was originally set. If a provided validator is not found, it
   * is ignored.
   *
   * When you add or remove a validator at run time, you must call
   * `updateValueAndValidity()` for the new validation to take effect.
   *
   * @param validators The asynchronous validator or validators to remove.
   */
  removeAsyncValidators(validators: AsyncValidatorFn | AsyncValidatorFn[]): void {
    this.setAsyncValidators(removeValidators(validators, this._rawAsyncValidators));
  }

  /**
   * Check whether a synchronous validator function is present on this control. The provided
   * validator must be a reference to the exact same function that was provided.
   *
   * @usageNotes
   *
   * ### Reference to a ValidatorFn
   *
   * ```
   * // Reference to the RequiredValidator
   * const ctrl = new FormControl<number | null>(0, Validators.required);
   * expect(ctrl.hasValidator(Validators.required)).toEqual(true)
   *
   * // Reference to anonymous function inside MinValidator
   * const minValidator = Validators.min(3);
   * const ctrl = new FormControl<number | null>(0, minValidator);
   * expect(ctrl.hasValidator(minValidator)).toEqual(true)
   * expect(ctrl.hasValidator(Validators.min(3))).toEqual(false)
   * ```
   *
   * @param validator The validator to check for presence. Compared by function reference.
   * @returns Whether the provided validator was found on this control.
   */
  hasValidator(validator: ValidatorFn): boolean {
    return hasValidator(this._rawValidators, validator);
  }

  /**
   * Check whether an asynchronous validator function is present on this control. The provided
   * validator must be a reference to the exact same function that was provided.
   *
   * @param validator The asynchronous validator to check for presence. Compared by function
   *     reference.
   * @returns Whether the provided asynchronous validator was found on this control.
   */
  hasAsyncValidator(validator: AsyncValidatorFn): boolean {
    return hasValidator(this._rawAsyncValidators, validator);
  }

  /**
   * Empties out the synchronous validator list.
   *
   * When you add or remove a validator at run time, you must call
   * `updateValueAndValidity()` for the new validation to take effect.
   *
   */
  clearValidators(): void {
    this.validator = null;
  }

  /**
   * Empties out the async validator list.
   *
   * When you add or remove a validator at run time, you must call
   * `updateValueAndValidity()` for the new validation to take effect.
   *
   */
  clearAsyncValidators(): void {
    this.asyncValidator = null;
  }

  /**
   * Marks the c


or(shouldHaveEmitted: boolean, emitEvent?: boolean): void {
    if (this.asyncValidator) {
      this.status = PENDING;
      this._hasOwnPendingAsyncValidator = {emitEvent: emitEvent !== false};
      const obs = toObservable(this.asyncValidator(this));
      this._asyncValidationSubscription = obs.subscribe((errors: ValidationErrors | null) => {
        this._hasOwnPendingAsyncValidator = null;
        // This will trigger the recalculation of the validation status, which depends on
        // the state of the asynchronous validation (whether it is in progress or not). So, it is
        // necessary that we have updated the `_hasOwnPendingAsyncValidator` boolean flag first.
        this.setErrors(errors, {emitEvent, shouldHaveEmitted});
      });
    }
  }

  private _cancelExistingSubscription(): boolean {
    if (this._asyncValidationSubscription) {
      this._asyncValidationSubscription.unsubscribe();

      // we're cancelling the validator subscribtion, we keep if it should have emitted
      // because we want to emit eventually if it was required at least once.
      const shouldHaveEmitted = this._hasOwnPendingAsyncValidator?.emitEvent ?? false;
      this._hasOwnPendingAsyncValidator = null;
      return shouldHaveEmitted;
    }
    return false;
  }

  /**
   * Sets errors on a form control when running validations manually, rather than automatically.
   *
   * Calling `setErrors` also updates the validity of the parent control.
   *
   * @param opts Configuration options that determine how the control propagates
   * changes and emits events after the control errors are set.
   * * `emitEvent`: When true or not supplied (the default), the `statusChanges`
   * observable emits an event after the errors are set.
   *
   * @usageNotes
   *
   * ### Manually set the errors for a control
   *
   * ```
   * const login = new FormControl('someLogin');
   * login.setErrors({
   *   notUnique: true
   * });
   *
   * expect(login.valid).toEqual(false);
   * expect(login.errors).toEqual({ notUnique: true });
   *
   * login.setValue('someOtherLogin');
   *
   * expect(login.valid).toEqual(true);
   * ```
   */
  setErrors(errors: ValidationErrors | null, opts?: {emitEvent?: boolean}): void;

  /** @internal */
  setErrors(
    errors: ValidationErrors | null,
    opts?: {emitEvent?: boolean; shouldHaveEmitted?: boolean},
  ): void;
  setErrors(
    errors: ValidationErrors | null,
    opts: {emitEvent?: boolean; shouldHaveEmitted?: boolean} = {},
  ): void {
    (this as Writable<this>).errors = errors;
    this._updateControlsErrors(opts.emitEvent !== false, this, opts.shouldHaveEmitted);
  }

  /**
   * Retrieves a child control given the control's name or path.
   *
   * This signature for get supports strings and `const` arrays (`.get(['foo', 'bar'] as const)`).
   */
  get<P extends string | readonly (string | number)[]>(
    path: P,
  ): AbstractControl<ɵGetProperty<TRawValue, P>> | null;

  /**
   * Retrieves a child control given the control's name or path.
   *
   * This signature for `get` supports non-const (mutable) arrays. Inferred type
   * information will not be as robust, so prefer to pass a `readonly` array if possible.
   */
  get<P extends string | Array<string | number>>(
    path: P,
  ): AbstractControl<ɵGetProperty<TRawValue, P>> | null;

  /**
   * Retrieves a child control given the control's name or path.
   *
   * @param path A dot-delimited string or array of string/number values that define the path to the
   * control. If a string is provided, passing it as a string literal will result in improved type
   * information. Likewise, if an array is provided, passing it `as const` will cause improved type
   * information to be available.
   *
   * @usageNotes
   * ### Retrieve a nested control
   *
   * For example, to get a `name` control nested within a `person` sub-group:
   *
   * * `this.form.get('person.name');`
   *
   * -OR-
   *
   * * `this.form.get(['person', 'name'] as const);` // `as const` gives improved typings
   *
   * ### Retrieve a control in a FormArray
   *
   * When accessing an element inside a FormArray, you can use an element index.
   * For example, to get a `price` control from the first element in an `items` array you can use:
   *
   * * `this.form.get('items.0.price');`
   *
   * -OR-
   *
   * * `this.form.get(['items', 0, 'price']);`
   */
  get<P extends string | (string | number)[]>(
    path: P,
  ): AbstractControl<ɵGetProperty<TRawValue, P>> | null {
    let currPath: Array<string | number> | string = path;
    if (currPath == null) return null;
    if (!Array.isArray(currPath)) currPath = currPath.split('.');
    if (currPath.length === 0) return null;
    return currPath.reduce(
      (control: AbstractControl | null, name) => control && control._find(name),
      this,
    );
  }

  /**
   * @description
   * Reports error data for the control with the given path.
   *
   * @param errorCode The code of the error to check
   * @param path A list of control names that designates how to move from the current control
   * to the control that should be queried for errors.
   *
   * @usageNotes
   * For example, for the following `FormGroup`:
   *
   * ```
   * form = new FormGroup({
   *   address: new FormGroup({ street: new FormControl() })
   * });
   * ```
   *
   * The path to the 'street' control from the root form would be 'address' -> 'street'.
   *
   * It can be provided to this method in one of two formats:
   *
   * 1. An array of string control names, e.g. `['address', 'street']`
   * 1. A period-delimited list of control names in one string, e.g. `'address.street'`
   *
   * @returns error data for that particular error. If the control or error is not present,
   * null is returned.
   */
  getError(errorCode: string, path?: Array<string | number> | string): any {
    const control = path ? this.get(path) : this;
    return control && control.errors ? control.errors[errorCode] : null;
  }

  /**
   * @description
   * Reports whether the control with the given path has the error specified.
   *
   * @param errorCode The code of the error to check
   * @param path A list of control names that designates how to move from the current control
   * to the control that should be queried for errors.
   *
   * @usageNotes
   * For example, for the following `FormGroup`:
   *
   * ```
   * form = new FormGroup({
   *   address: new FormGroup({ street: new FormControl() })
   * });
   * ```
   *
   * The path to the 'street' control from the root form would be 'address' -> 'street'.
   *
   * It can be provided to this method in one of two formats:
   *
   * 1. An array of string control names, e.g. `['address', 'street']`
   * 1. A period-delimited list of control names in one string, e.g. `'address.street'`
   *
   * If no path is given, this method checks for the error on the current control.
   *
   * @returns whether the given error is present in the control at the given path.
   *
   * If the control is not present, false is returned.
   */
  hasError(errorCode: string, path?: Array<string | number> | string): boolean {
    return !!this.getError(errorCode, path);
  }

  /**
   * Retrieves the top-level ancestor of this control.
   */
  get root(): AbstractControl {
    let x: AbstractControl = this;

    while (x._parent) {
      x = x._parent;
    }

    return x;
  }

  /** @internal */
  _updateControlsErrors(


describe('Angular with zoneless enabled', () => {
  async function createFixture<T>(type: Type<T>): Promise<ComponentFixture<T>> {
    const fixture = TestBed.createComponent(type);
    await fixture.whenStable();
    return fixture;
  }
  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [
        provideZonelessChangeDetection(),
        {provide: PLATFORM_ID, useValue: PLATFORM_BROWSER_ID},
      ],
    });
  });

  describe('notifies scheduler', () => {
    it('contributes to application stableness', async () => {
      const val = signal('initial');
      @Component({template: '{{val()}}', standalone: true})
      class TestComponent {
        val = val;
      }
      const fixture = await createFixture(TestComponent);

      // Cause another pending CD immediately after render and verify app has not stabilized
      await fixture.whenStable().then(() => {
        val.set('new');
      });
      expect(fixture.isStable()).toBeFalse();

      await fixture.whenStable();
      expect(fixture.isStable()).toBeTrue();
    });

    it('when signal updates', async () => {
      const val = signal('initial');
      @Component({template: '{{val()}}', standalone: true})
      class TestComponent {
        val = val;
      }

      const fixture = await createFixture(TestComponent);
      expect(fixture.nativeElement.innerText).toEqual('initial');

      val.set('new');
      expect(fixture.isStable()).toBeFalse();
      await fixture.whenStable();
      expect(fixture.nativeElement.innerText).toEqual('new');
    });

    it('when using markForCheck()', async () => {
      @Component({template: '{{val}}', standalone: true})
      class TestComponent {
        cdr = inject(ChangeDetectorRef);
        val = 'initial';
        setVal(val: string) {
          this.val = val;
          this.cdr.markForCheck();
        }
      }

      const fixture = await createFixture(TestComponent);
      expect(fixture.nativeElement.innerText).toEqual('initial');

      fixture.componentInstance.setVal('new');
      expect(fixture.isStable()).toBe(false);
      await fixture.whenStable();
      expect(fixture.nativeElement.innerText).toEqual('new');
    });

    it('on input binding', async () => {
      @Component({template: '{{val}}', standalone: true})
      class TestComponent {
        @Input() val = 'initial';
      }

      const fixture = await createFixture(TestComponent);
      expect(fixture.nativeElement.innerText).toEqual('initial');

      fixture.componentRef.setInput('val', 'new');
      expect(fixture.isStable()).toBe(false);
      await fixture.whenStable();
      expect(fixture.nativeElement.innerText).toEqual('new');
    });

    it('on event listener bound in template', async () => {
      @Component({template: '<div (click)="updateVal()">{{val}}</div>', standalone: true})
      class TestComponent {
        val = 'initial';

        updateVal() {
          this.val = 'new';
        }
      }

      const fixture = await createFixture(TestComponent);
      expect(fixture.nativeElement.innerText).toEqual('initial');

      fixture.debugElement
        .query((p) => p.nativeElement.tagName === 'DIV')
        .triggerEventHandler('click');
      expect(fixture.isStable()).toBe(false);
      await fixture.whenStable();
      expect(fixture.nativeElement.innerText).toEqual('new');
    });

    it('on event listener bound in host', async () => {
      @Component({host: {'(click)': 'updateVal()'}, template: '{{val}}', standalone: true})
      class TestComponent {
        val = 'initial';

        updateVal() {
          this.val = 'new';
        }
      }

      const fixture = await createFixture(TestComponent);
      expect(fixture.nativeElement.innerText).toEqual('initial');

      fixture.debugElement.triggerEventHandler('click');
      expect(fixture.isStable()).toBe(false);
      await fixture.whenStable();
      expect(fixture.nativeElement.innerText).toEqual('new');
    });

    it('with async pipe', async () => {
      @Component({template: '{{val | async}}', standalone: true, imports: [AsyncPipe]})
      class TestComponent {
        val = new BehaviorSubject('initial');
      }

      const fixture = await createFixture(TestComponent);
      expect(fixture.nativeElement.innerText).toEqual('initial');

      fixture.componentInstance.val.next('new');
      expect(fixture.isStable()).toBe(false);
      await fixture.whenStable();
      expect(fixture.nativeElement.innerText).toEqual('new');
    });

    it('when creating a view', async () => {
      @Component({
        template: '<ng-template #ref>{{"binding"}}</ng-template>',
        standalone: true,
      })
      class TestComponent {
        @ViewChild(TemplateRef) template!: TemplateRef<unknown>;
        @ViewChild('ref', {read: ViewContainerRef}) viewContainer!: ViewContainerRef;

        createView(): any {
          this.viewContainer.createEmbeddedView(this.template);
        }
      }

      const fixture = await createFixture(TestComponent);

      fixture.componentInstance.createView();
      expect(fixture.isStable()).toBe(false);
      await fixture.whenStable();
      expect(fixture.nativeElement.innerText).toEqual('binding');
    });

    it('when inserting a view', async () => {
      @Component({
        template: '{{"binding"}}',
        standalone: true,
      })
      class DynamicCmp {
        elementRef = inject(ElementRef);
      }
      @Component({
        template: '<ng-template #ref></ng-template>',
        standalone: true,
      })
      class TestComponent {
        @ViewChild('ref', {read: ViewContainerRef}) viewContainer!: ViewContainerRef;
      }

      const fixture = await createFixture(TestComponent);

      const otherComponent = createComponent(DynamicCmp, {
        environmentInjector: TestBed.inject(EnvironmentInjector),
      });
      fixture.componentInstance.viewContainer.insert(otherComponent.hostView);
      expect(fixture.isStable()).toBe(false);
      await fixture.whenStable();
      expect(fixture.nativeElement.innerText).toEqual('binding');
    });

    it('when destroying a view (with animations)', async () => {
      @Component({
        template: '{{"binding"}}',
        standalone: true,
      })
      class DynamicCmp {
        elementRef = inject(ElementRef);
      }
      @Component({
        template: '<ng-template #ref></ng-template>',
        standalone: true,
      })
      class TestComponent {
        @ViewChild('ref', {read: ViewContainerRef}) viewContainer!: ViewContainerRef;
      }

      const fixture = await createFixture(TestComponent);
      const component = createComponent(DynamicCmp, {
        environmentInjector: TestBed.inject(EnvironmentInjector),
      });

      fixture.componentInstance.viewContainer.insert(component.hostView);
      await fixture.whenStable();
      expect(fixture.nativeElement.innerText).toEqual('binding');
      fixture.componentInstance.viewContainer.remove();
      await fixture.whenStable();
      expect(fixture.nativeElement.innerText).toEqual('');

      const component2 = createComponent(DynamicCmp, {
        environmentInjector: TestBed.inject(EnvironmentInjector),
      });
      fixture.componentInstance.viewContainer.insert(component2.hostView);
      await fixture.whenStable();
      expect(fixture.nativeElement.innerText).toEqual('binding');
      component2.destroy();
      await fixture.whenStable();
      expect(fixture.nativeElement.innerText).toEqual('');
    });

    function whenStable(): Promise<void> {
      return TestBed.inject(ApplicationRef).whenStable();
    }

    it(
      'when destroying a view (*no* animations)',
      withBody('<app></app>', async () => {
        destroyPlatform();
        let doCheckCount = 0;
        let renderHookCalls = 0;
        @Component({
          template: '{{"binding"}}',
          standalone: true,
        })
        class DynamicCmp {
          elementRef = inject(ElementRef);
        }
        @Component({
          selector: 'app',
          template: '<ng-template #ref></ng-template>',
          standalone: true,
        })
        class App {
          @ViewChild('ref', {read: ViewContainerRef}) viewContainer!: ViewContainerRef;
          unused = afterRender(() => {
            renderHookCalls++;
          });

          ngDoCheck() {
            doCheckCount++;
          }
        }
        const applicationRef = await bootstrapApplication(App, {
          providers: [
            provideZonelessChangeDetection(),
            {provide: PLATFORM_ID, useValue: PLATFORM_BROWSER_ID},
          ],
        });
        const appViewRef = (applicationRef as any)._views[0] as {context: App; rootNodes: any[]};
        await applicationRef.whenStable();

        const component2 = createComponent(DynamicCmp, {
          environmentInjector: applicationRef.injector,
        });
        appViewRef.context.viewContainer.insert(component2.hostView);
        expect(isStable(applicationRef.injector)).toBe(false);
        await applicationRef.whenStable();
        component2.destroy();

        // destroying the view synchronously removes element from DOM when not using animations
        expect(appViewRef.rootNodes[0].innerText).toEqual('');
        // Destroying the view notifies scheduler because render hooks need to run
        expect(isStable(applicationRef.injector)).toBe(false);

        let checkCountBeforeStable = doCheckCount;
        let renderCountBeforeStable = renderHookCalls;
        await applicationRef.whenStable();
        // The view should not have refreshed
        expect(doCheckCount).toEqual(checkCountBeforeStable);
        // but render hooks should have run
        expect(renderHookCalls).toEqual(renderCountBeforeStable + 1);

        destroyPlatform();
      }),
    );


describe('Angular with scheduler and ZoneJS', () => {
  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [
        {provide: ComponentFixtureAutoDetect, useValue: true},
        {provide: PLATFORM_ID, useValue: PLATFORM_BROWSER_ID},
      ],
    });
  });

  it('requires updates inside Angular zone when using ngZoneOnly', async () => {
    TestBed.configureTestingModule({
      providers: [provideZoneChangeDetection({ignoreChangesOutsideZone: true})],
    });
    @Component({template: '{{thing()}}', standalone: true})
    class App {
      thing = signal('initial');
    }

    const fixture = TestBed.createComponent(App);
    await fixture.whenStable();
    expect(fixture.nativeElement.innerText).toContain('initial');

    TestBed.inject(NgZone).runOutsideAngular(() => {
      fixture.componentInstance.thing.set('new');
    });
    expect(fixture.isStable()).toBe(true);
    await fixture.whenStable();
    expect(fixture.nativeElement.innerText).toContain('initial');
  });

  it('will not schedule change detection if listener callback is outside the zone', async () => {
    let renders = 0;
    TestBed.runInInjectionContext(() => {
      afterRender(() => {
        renders++;
      });
    });

    @Component({selector: 'component-with-output', template: '', standalone: true})
    class ComponentWithOutput {
      @Output() out = new EventEmitter();
    }
    let called = false;
    @Component({
      standalone: true,
      imports: [ComponentWithOutput],
      template: '<component-with-output (out)="onOut()" />',
    })
    class App {
      onOut() {
        called = true;
      }
    }
    const fixture = TestBed.createComponent(App);
    await fixture.whenStable();
    const outComponent = fixture.debugElement.query(
      (debugNode) => debugNode.providerTokens!.indexOf(ComponentWithOutput) !== -1,
    ).componentInstance as ComponentWithOutput;
    TestBed.inject(NgZone).runOutsideAngular(() => {
      outComponent.out.emit();
    });
    await fixture.whenStable();

    expect(renders).toBe(1);
    expect(called).toBe(true);
    expect(renders).toBe(1);
  });

  it('updating signal outside of zone still schedules update when in hybrid mode', async () => {
    @Component({template: '{{thing()}}', standalone: true})
    class App {
      thing = signal('initial');
    }

    const fixture = TestBed.createComponent(App);
    await fixture.whenStable();
    expect(fixture.nativeElement.innerText).toContain('initial');

    TestBed.inject(NgZone).runOutsideAngular(() => {
      fixture.componentInstance.thing.set('new');
    });
    expect(fixture.isStable()).toBe(false);
    await fixture.whenStable();
    expect(fixture.nativeElement.innerText).toContain('new');
  });

  it('updating signal in another "Angular" zone schedules update when in hybrid mode', async () => {
    @Component({template: '{{thing()}}', standalone: true})
    class App {
      thing = signal('initial');
    }

    const fixture = TestBed.createComponent(App);
    await fixture.whenStable();
    expect(fixture.nativeElement.innerText).toContain('initial');
    const differentAngularZone: NgZone = Zone.root.run(() => new NgZone({}));
    differentAngularZone.run(() => {
      fixture.componentInstance.thing.set('new');
    });

    expect(fixture.isStable()).toBe(false);
    await fixture.whenStable();
    expect(fixture.nativeElement.innerText).toContain('new');
  });

  it('updating signal in a child zone of Angular does not schedule extra CD', async () => {
    @Component({template: '{{thing()}}', standalone: true})
    class App {
      thing = signal('initial');
    }

    const fixture = TestBed.createComponent(App);
    await fixture.whenStable();
    expect(fixture.nativeElement.innerText).toContain('initial');
    const childZone = TestBed.inject(NgZone).run(() => Zone.current.fork({name: 'child'}));

    childZone.run(() => {
      fixture.componentInstance.thing.set('new');
      expect(TestBed.inject(ChangeDetectionSchedulerImpl)['cancelScheduledCallback']).toBeNull();
    });
  });

  it('updating signal in a child Angular zone of Angular does not schedule extra CD', async () => {
    @Component({template: '{{thing()}}', standalone: true})
    class App {
      thing = signal('initial');
    }

    const fixture = TestBed.createComponent(App);
    await fixture.whenStable();
    expect(fixture.nativeElement.innerText).toContain('initial');
    const childAngularZone = TestBed.inject(NgZone).run(() => new NgZone({}));

    childAngularZone.run(() => {
      fixture.componentInstance.thing.set('new');
      expect(TestBed.inject(ChangeDetectionSchedulerImpl)['cancelScheduledCallback']).toBeNull();
    });
  });

  it('should not run change detection twice if notified during AppRef.tick', async () => {
    TestBed.configureTestingModule({
      providers: [
        provideZoneChangeDetection({ignoreChangesOutsideZone: false}),
        {provide: PLATFORM_ID, useValue: PLATFORM_BROWSER_ID},
      ],
    });

    let changeDetectionRuns = 0;
    TestBed.runInInjectionContext(() => {
      afterRender(() => {
        changeDetectionRuns++;
      });
    });
    @Component({template: '', standalone: true})
    class MyComponent {
      cdr = inject(ChangeDetectorRef);
      ngDoCheck() {
        // notify scheduler every time this component is checked
        this.cdr.markForCheck();
      }
    }
    const fixture = TestBed.createComponent(MyComponent);
    await fixture.whenStable();
    expect(changeDetectionRuns).toEqual(1);

    // call tick manually
    TestBed.inject(ApplicationRef).tick();
    await fixture.whenStable();
    // ensure we only ran render hook 1 more time rather than once for tick and once for the
    // scheduled run
    expect(changeDetectionRuns).toEqual(2);
  });

  it('does not cause double change detection with run coalescing', async () => {
    if (isNode) {
      return;
    }

    TestBed.configureTestingModule({
      providers: [
        provideZoneChangeDetection({runCoalescing: true, ignoreChangesOutsideZone: false}),
      ],
    });
    @Component({template: '{{thing()}}', standalone: true})
    class App {
      thing = signal('initial');
    }
    const fixture = TestBed.createComponent(App);
    await fixture.whenStable();

    let ticks = 0;
    TestBed.runInInjectionContext(() => {
      afterRender(() => {
        ticks++;
      });
    });
    fixture.componentInstance.thing.set('new');
    await fixture.whenStable();
    expect(ticks).toBe(1);
  });

  it('does not cause double change detection with run coalescing when both schedulers are notified', async () => {
    if (isNode) {
      return;
    }

    TestBed.configureTestingModule({
      providers: [
        provideZoneChangeDetection({runCoalescing: true, ignoreChangesOutsideZone: false}),
      ],
    });
    @Component({template: '{{thing()}}', standalone: true})
    class App {
      thing = signal('initial');
    }
    const fixture = TestBed.createComponent(App);
    await fixture.whenStable();

    let ticks = 0;
    TestBed.runInInjectionContext(() => {
      afterRender(() => {
        ticks++;
      });
    });
    // notifies the zoneless scheduler
    fixture.componentInstance.thing.set('new');
    // notifies the zone scheduler
    TestBed.inject(NgZone).run(() => {});
    await fixture.whenStable();
    expect(ticks).toBe(1);
  });

  it('can run inside fakeAsync zone', fakeAsync(() => {
    TestBed.configureTestingModule({
      providers: [provideZoneChangeDetection({scheduleInRootZone: false} as any)],
    });
    let didRun = false;
    @Component({standalone: true, template: ''})
    class App {
      ngDoCheck() {
        didRun = true;
      }
    }

    // create component runs inside the zone and triggers CD as a result
    const fixture = TestBed.createComponent(App);
    didRun = false;

    // schedules change detection
    fixture.debugElement.injector.get(ChangeDetectorRef).markForCheck();
    expect(didRun).toBe(false);
    tick();
    expect(didRun).toBe(true);
  }));
});


/**
 * @license
 * Copyright Google LLC All Rights Reserved.
 *
 * Use of this source code is governed by an MIT-style license that can be
 * found in the LICENSE file at https://angular.dev/license
 */

import {
  ApplicationRef,
  ChangeDetectionStrategy,
  Component,
  effect,
  inject,
  Injector,
  Input,
  NgZone,
  provideZoneChangeDetection,
  signal,
} from '@angular/core';
import {TestBed} from '@angular/core/testing';
import {setUseMicrotaskEffectsByDefault} from '../src/render3/reactivity/effect';

describe('effects in TestBed', () => {
  let prev: boolean;
  beforeEach(() => {
    prev = setUseMicrotaskEffectsByDefault(false);
  });
  afterEach(() => setUseMicrotaskEffectsByDefault(prev));

  it('created in the constructor should run with detectChanges()', () => {
    const log: string[] = [];
    @Component({
      selector: 'test-cmp',
      standalone: true,
      template: '',
    })
    class Cmp {
      constructor() {
        log.push('Ctor');

        effect(() => {
          log.push('Effect');
        });
      }

      ngDoCheck() {
        log.push('DoCheck');
      }
    }

    TestBed.createComponent(Cmp).detectChanges();

    expect(log).toEqual([
      // The component gets constructed, which creates the effect. Since the effect is created in a
      // component, it doesn't get scheduled until the component is first change detected.
      'Ctor',

      // Next, the first change detection (update pass) happens.
      'DoCheck',

      // Then the effect runs.
      'Effect',
    ]);
  });

  it('created in ngOnInit should run with detectChanges()', () => {
    const log: string[] = [];
    @Component({
      selector: 'test-cmp',
      standalone: true,
      template: '',
    })
    class Cmp {
      private injector = inject(Injector);

      constructor() {
        log.push('Ctor');
      }

      ngOnInit() {
        effect(
          () => {
            log.push('Effect');
          },
          {injector: this.injector},
        );
      }

      ngDoCheck() {
        log.push('DoCheck');
      }
    }

    TestBed.createComponent(Cmp).detectChanges();

    expect(log).toEqual([
      // The component gets constructed.
      'Ctor',

      // Next, the first change detection (update pass) happens, which creates the effect and
      // schedules it for execution.
      'DoCheck',

      // Then the effect runs.
      'Effect',
    ]);
  });

  it('will flush effects automatically when using autoDetectChanges', async () => {
    const val = signal('initial');
    let observed = '';
    @Component({
      selector: 'test-cmp',
      standalone: true,
      template: '',
    })
    class Cmp {
      constructor() {
        effect(() => {
          observed = val();
        });
      }
    }

    const fixture = TestBed.createComponent(Cmp);
    fixture.autoDetectChanges();

    expect(observed).toBe('initial');
    val.set('new');
    expect(observed).toBe('initial');
    await fixture.whenStable();
    expect(observed).toBe('new');
  });

  it('will run an effect with an input signal on the first CD', () => {
    let observed: string | null = null;

    @Component({
      standalone: true,
      template: '',
    })
    class Cmp {
      @Input() input!: string;
      constructor() {
        effect(() => {
          observed = this.input;
        });
      }
    }

    const fix = TestBed.createComponent(Cmp);
    fix.componentRef.setInput('input', 'hello');
    fix.detectChanges();

    expect(observed as string | null).toBe('hello');
  });

  it('should run root effects before detectChanges() when in zone mode', async () => {
    TestBed.configureTestingModule({
      providers: [provideZoneChangeDetection()],
    });
    const log: string[] = [];

    @Component({
      standalone: true,
      template: `{{ sentinel }}`,
    })
    class TestCmp {
      get sentinel(): string {
        log.push('CD');
        return '';
      }
    }

    // Instantiate the component and CD it once.
    const fix = TestBed.createComponent(TestCmp);
    fix.detectChanges();

    // Instantiate a root effect and run it once.
    const counter = signal(0);
    const appRef = TestBed.inject(ApplicationRef);
    effect(() => log.push(`effect: ${counter()}`), {injector: appRef.injector});
    await appRef.whenStable();

    log.length = 0;

    // Trigger the effect and call `detectChanges()` on the fixture.
    counter.set(1);
    fix.detectChanges(false);

    // The effect should run before the component CD.
    expect(log).toEqual(['effect: 1', 'CD']);
  });
});


describe('injection flags', () => {
      it('should be able to optionally inject a token', () => {
        const TOKEN = new InjectionToken<string>('TOKEN');

        expect(TestBed.inject(TOKEN, undefined, {optional: true})).toBeNull();
        expect(TestBed.inject(TOKEN, undefined, InjectFlags.Optional)).toBeNull();

        expect(TestBed.inject(TOKEN, undefined, {optional: true})).toBeNull();
        expect(TestBed.inject(TOKEN, undefined, InjectFlags.Optional)).toBeNull();
      });

      it('should include `null` into the result type when the optional flag is used', () => {
        const TOKEN = new InjectionToken<string>('TOKEN');

        const flags: InjectOptions = {optional: true};
        let result = TestBed.inject(TOKEN, undefined, flags);
        expect(result).toBe(null);

        // Verify that `null` can be a valid value (from typing standpoint),
        // the line below would fail a type check in case the result doesn't
        // have `null` in the type.
        result = null;
      });

      it('should be able to use skipSelf injection', () => {
        const TOKEN = new InjectionToken<string>('TOKEN');
        TestBed.configureTestingModule({
          providers: [{provide: TOKEN, useValue: 'from TestBed'}],
        });

        expect(TestBed.inject(TOKEN)).toBe('from TestBed');

        expect(TestBed.inject(TOKEN, undefined, {skipSelf: true, optional: true})).toBeNull();
        expect(
          TestBed.inject(TOKEN, undefined, InjectFlags.SkipSelf | InjectFlags.Optional),
        ).toBeNull();
      });
    });
  });

  it('should be able to call Testbed.runInInjectionContext in tests', () => {
    const expectedValue = 'testValue';
    @Injectable({providedIn: 'root'})
    class SomeInjectable {
      readonly instanceValue = expectedValue;
    }

    function functionThatUsesInject(): string {
      return inject(SomeInjectable).instanceValue;
    }

    expect(TestBed.runInInjectionContext(functionThatUsesInject)).toEqual(expectedValue);
  });
});

describe('TestBed defer block behavior', () => {
  beforeEach(() => {
    TestBed.resetTestingModule();
  });

  it('should default defer block behavior to playthrough', () => {
    expect(TestBedImpl.INSTANCE.getDeferBlockBehavior()).toBe(DeferBlockBehavior.Playthrough);
  });

  it('should be able to configure defer block behavior', () => {
    TestBed.configureTestingModule({deferBlockBehavior: DeferBlockBehavior.Manual});
    expect(TestBedImpl.INSTANCE.getDeferBlockBehavior()).toBe(DeferBlockBehavior.Manual);
  });

  it('should reset the defer block behavior back to the default when TestBed is reset', () => {
    TestBed.configureTestingModule({deferBlockBehavior: DeferBlockBehavior.Manual});
    expect(TestBedImpl.INSTANCE.getDeferBlockBehavior()).toBe(DeferBlockBehavior.Manual);
    TestBed.resetTestingModule();
    expect(TestBedImpl.INSTANCE.getDeferBlockBehavior()).toBe(DeferBlockBehavior.Playthrough);
  });
});

describe('TestBed module teardown', (


describe('No NgZone', () => {
    beforeEach(() => {
      TestBed.configureTestingModule({
        providers: [{provide: ComponentFixtureNoNgZone, useValue: true}],
      });
    });

    it('calling autoDetectChanges raises an error', () => {
      const componentFixture = TestBed.createComponent(SimpleComp);
      expect(() => {
        componentFixture.autoDetectChanges();
      }).toThrowError(/Cannot call autoDetectChanges when ComponentFixtureNoNgZone is set/);
    });

    it('should instantiate a component with valid DOM', waitForAsync(() => {
      const componentFixture = TestBed.createComponent(SimpleComp);

      expect(componentFixture.ngZone).toBeNull();
      componentFixture.detectChanges();
      expect(componentFixture.nativeElement).toHaveText('Original Simple');
    }));

    it('should allow changing members of the component', waitForAsync(() => {
      const componentFixture = TestBed.createComponent(MyIfComp);

      componentFixture.detectChanges();
      expect(componentFixture.nativeElement).toHaveText('MyIf()');

      componentFixture.componentInstance.showMore = true;
      componentFixture.detectChanges();
      expect(componentFixture.nativeElement).toHaveText('MyIf(More)');
    }));

    it('throws errors that happen during detectChanges', () => {
      @Component({
        template: '',
        standalone: true,
      })
      class App {
        ngOnInit() {
          throw new Error();
        }
      }

      const fixture = TestBed.createComponent(App);
      expect(() => fixture.detectChanges()).toThrow();
    });
  });

  it('reports errors from autoDetect change detection to error handler', () => {
    let throwError = false;
    @Component({
      template: '',
      standalone: false,
    })
    class TestComponent {
      ngDoCheck() {
        if (throwError) {
          throw new Error();
        }
      }
    }
    const fixture = TestBed.createComponent(TestComponent);
    fixture.autoDetectChanges();
    const errorHandler = TestBed.inject(ErrorHandler);
    const spy = spyOn(errorHandler, 'handleError').and.callThrough();

    throwError = true;
    TestBed.inject(NgZone).run(() => {});
    expect(spy).toHaveBeenCalled();
  });

  it('reports errors from checkNoChanges in autoDetect to error handler', () => {
    let throwError = false;
    @Component({
      template: '{{thing}}',
      standalone: false,
    })
    class TestComponent {
      thing = 'initial';
      ngAfterViewChecked() {
        if (throwError) {
          this.thing = 'new';
        }
      }
    }
    const fixture = TestBed.createComponent(TestComponent);
    fixture.autoDetectChanges();
    const errorHandler = TestBed.inject(ErrorHandler);
    const spy = spyOn(errorHandler, 'handleError').and.callThrough();

    throwError = true;
    TestBed.inject(NgZone).run(() => {});
    expect(spy).toHaveBeenCalled();
  });
});

describe('ComponentFixture with zoneless', () => {
  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [
        provideExperimentalZonelessChangeDetection(),
        {provide: ErrorHandler, useValue: {handleError: () => {}}},
      ],
    });
  });

  it('will not refresh CheckAlways views when detectChanges is called if not marked dirty', () => {
    @Component({standalone: true, template: '{{signalThing()}}|{{regularThing}}'})
    class CheckAlwaysCmp {
      regularThing = 'initial';
      signalThing = signal('initial');
    }

    const fixture = TestBed.createComponent(CheckAlwaysCmp);
    // components are created dirty
    fixture.detectChanges();
    expect(fixture.nativeElement.innerText).toEqual('initial|initial');
    fixture.componentInstance.regularThing = 'new';
    // Expression changed after checked
    expect(() => fixture.detectChanges()).toThrow();
    expect(fixture.nativeElement.innerText).toEqual('initial|initial');
    fixture.componentInstance.signalThing.set('new');
    fixture.detectChanges();
    expect(fixture.nativeElement.innerText).toEqual('new|new');
  });

  it('throws errors that happen during detectChanges', () => {
    @Component({
      template: '',
      standalone: true,
    })
    class App {
      ngOnInit() {
        throw new Error();
      }
    }

    const fixture = TestBed.createComponent(App);
    expect(() => fixture.detectChanges()).toThrow();
  });

  it('rejects whenStable promise when errors happen during detectChanges', async () => {
    @Component({
      template: '',
      standalone: true,
    })
    class App {
      ngOnInit() {
        throw new Error();
      }
    }

    const fixture = TestBed.createComponent(App);
    await expectAsync(fixture.whenStable()).toBeRejected();
  });

  it('can disable checkNoChanges', () => {
    @Component({
      template: '{{thing}}',
      standalone: true,
    })
    class App {
      thing = 1;
      ngAfterViewChecked() {
        ++this.thing;
      }
    }

    const fixture = TestBed.createComponent(App);
    expect(() => fixture.detectChanges(false /*checkNoChanges*/)).not.toThrow();
    // still throws if checkNoChanges is not disabled
    expect(() => fixture.detectChanges()).toThrowError(/ExpressionChanged/);
  });

  it('runs change detection when autoDetect is false', () => {
    @Component({
      template: '{{thing()}}',
      standalone: true,
    })
    class App {
      thing = signal(1);
    }

    const fixture = TestBed.createComponent(App);
    fixture.autoDetectChanges(false);
    fixture.componentInstance.thing.set(2);
    fixture.detectChanges();
    expect(fixture.nativeElement.innerText).toBe('2');
  });
});


escribe('projectable nodes', () => {
    @Component({
      selector: 'test',
      template: '',
      standalone: false,
    })
    class TestComponent {
      constructor(public vcr: ViewContainerRef) {}
    }

    @Component({
      selector: 'with-content',
      template: '',
      standalone: false,
    })
    class WithContentCmpt {
      @ViewChild('ref', {static: true}) directiveRef: any;
    }

    @Component({
      selector: 're-project',
      template: '<ng-content></ng-content>',
      standalone: false,
    })
    class ReProjectCmpt {}

    @Directive({
      selector: '[insert]',
      standalone: false,
    })
    class InsertTplRef implements OnInit {
      constructor(
        private _vcRef: ViewContainerRef,
        private _tplRef: TemplateRef<{}>,
      ) {}

      ngOnInit() {
        this._vcRef.createEmbeddedView(this._tplRef);
      }
    }

    @Directive({
      selector: '[delayedInsert]',
      exportAs: 'delayedInsert',
      standalone: false,
    })
    class DelayedInsertTplRef {
      constructor(
        public vc: ViewContainerRef,
        public templateRef: TemplateRef<Object>,
      ) {}
      show() {
        this.vc.createEmbeddedView(this.templateRef);
      }
      hide() {
        this.vc.clear();
      }
    }

    let fixture: ComponentFixture<TestComponent>;

    function createCmptInstance(
      tpl: string,
      projectableNodes: any[][],
    ): ComponentRef<WithContentCmpt> {
      TestBed.configureTestingModule({
        declarations: [
          WithContentCmpt,
          InsertTplRef,
          DelayedInsertTplRef,
          ReProjectCmpt,
          TestComponent,
        ],
      });
      TestBed.overrideTemplate(WithContentCmpt, tpl);

      fixture = TestBed.createComponent(TestComponent);
      const vcr = fixture.componentInstance.vcr;
      const cmptRef = vcr.createComponent(WithContentCmpt, {
        injector: Injector.NULL,
        projectableNodes,
      });

      cmptRef.changeDetectorRef.detectChanges();

      return cmptRef;
    }

    it('should pass nodes to the default ng-content without selectors', () => {
      const cmptRef = createCmptInstance('
(<ng-content></ng-content>)
', [
        [document.createTextNode('A')],
      ]);
      expect(cmptRef.location.nativeElement).toHaveText('(A)');
    });

    it('should pass nodes to the default ng-content at the root', () => {
      const cmptRef = createCmptInstance('<ng-content></ng-content>', [
        [document.createTextNode('A')],
      ]);
      expect(cmptRef.location.nativeElement).toHaveText('A');
    });

    it('should pass nodes to multiple ng-content tags', () => {
      const cmptRef = createCmptInstance(
        'A:(<ng-content></ng-content>)B:(<ng-content select="b"></ng-content>)C:(<ng-content select="c"></ng-content>)',
        [
          [document.createTextNode('A')],
          [document.createTextNode('B')],
          [document.createTextNode('C')],
        ],
      );
      expect(cmptRef.location.nativeElement).toHaveText('A:(A)B:(B)C:(C)');
    });

    it('should pass nodes to the default ng-content inside ng-container', () => {
      const cmptRef = createCmptInstance(
        'A<ng-container>(<ng-content></ng-content>)</ng-container>C',
        [[document.createTextNode('B')]],
      );
      expect(cmptRef.location.nativeElement).toHaveText('A(B)C');
    });

    it('should pass nodes to the default ng-content inside an embedded view', () => {
      const cmptRef = createCmptInstance(
        'A<ng-template insert>(<ng-content></ng-content>)</ng-template>C',
        [[document.createTextNode('B')]],
      );
      expect(cmptRef.location.nativeElement).toHaveText('A(B)C');
    });

    it('should pass nodes to the default ng-content inside a delayed embedded view', () => {
      const cmptRef = createCmptInstance(
        'A(<ng-template #ref="delayedInsert" delayedInsert>[<ng-content></ng-content>]</ng-template>)C',
        [[document.createTextNode('B')]],
      );
      expect(cmptRef.location.nativeElement).toHaveText('A()C');

      const delayedInsert = cmptRef.instance.directiveRef as DelayedInsertTplRef;

      delayedInsert.show();
      cmptRef.changeDetectorRef.detectChanges();
      expect(cmptRef.location.nativeElement).toHaveText('A([B])C');

      delayedInsert.hide();
      cmptRef.changeDetectorRef.detectChanges();
      expect(cmptRef.location.nativeElement).toHaveText('A()C');
    });

    it('should re-project at the root', () => {
      const cmptRef = createCmptInstance(
        'A[<re-project>(<ng-content></ng-content>)</re-project>]C',
        [[document.createTextNode('B')]],
      );
      expect(cmptRef.location.nativeElement).toHaveText('A[(B)]C');
    });
  });
});

@Component({
  selector: 'main',
  template: '',
  standalone: false,
})
class MainComp {
  text: string = '';
}

@Component({
  selector: 'other',
  template: '',
  standalone: false,
})
class OtherComp {
  text: string = '';
}

@Component({
  selector: 'simple',
  inputs: ['stringProp'],
  template: 'SIMPLE(<ng-content></ng-content>)',
  standalone: false,
})
class Simple {
  stringProp: string = '';
}

@Component({
  selector: 'simple-shadow-dom1',
  template: 'SIMPLE1(<slot></slot>)',
  encapsulation: ViewEncapsulation.ShadowDom,
  styles: ['div {color: red}'],
  standalone: false,
})
class SimpleShadowDom1 {}

@Component({
  selector: 'simple-shadow-dom2',
  template: 'SIMPLE2(<slot></slot>)',
  encapsulation: ViewEncapsulation.ShadowDom,
  styles: ['div {color: blue}'],
  standalone: false,
})
class SimpleShadowDom2 {}

@Component({
  selector: 'empty',
  template: '',
  standalone: false,
})
class Empty {}

@Component({
  selector: 'multiple-content-tags',
  template: '(<ng-content SELECT=".left"></ng-content>, <ng-content></ng-content>)',
  standalone: false,
})
class MultipleContentTagsComponent {}

@Component({
  selector: 'single-content-tag',
  template: '<ng-content SELECT=".target"></ng-content>',
  standalone: false,
})
class SingleContentTagComponent {}

@Directive({
  selector: '[manual]',
  standalone: false,
})
class ManualViewportDirective {
  constructor(
    public vc: ViewContainerRef,
    public templateRef: TemplateRef<Object>,
  ) {}
  show() {
    this.vc.createEmbeddedView(this.templateRef);
  }
  hide() {
    this.vc.clear();
  }
}

@Directive({
  selector: '[project]',
  standalone: false,
})
class ProjectDirective {
  constructor(public vc: ViewContainerRef) {}
  show(templateRef: TemplateRef<Object>) {
    this.vc.createEmbeddedView(templateRef);
  }
  hide() {
    this.vc.clear();
  }
}

@Component({
  selector: 'outer-with-indirect-nested',
  template: 'OUTER(<simple>
<ng-content></ng-content>
</simple>)',
  standalone: false,
})
class OuterWithIndirectNestedComponent {}

@Component({
  selector: 'outer',
  template:
    'OUTER(<inner><ng-content select=".left" class="left"></ng-content><ng-content></ng-content></inner>)',
  standalone: false,
})
class OuterComponent {}

@Component({
  selector: 'inner',
  template:
    'INNER(<innerinner><ng-content select=".left" class="left"></ng-content><ng-content></ng-content></innerinner>)',
  standalone: false,
})
class InnerComponent {}

@Component({
  selector: 'innerinner',
  template: 'INNERINNER(<ng-content select=".left"></ng-content>,<ng-content></ng-content>)',
  standalone: false,
})
class InnerInnerComponent {}

@Component({
  selector: 'conditional-content',
  template:
    '
(
<ng-content select=".left"></ng-content>
, <ng-content></ng-content>)
',
  standalone: false,
})
class ConditionalContentComponent {}

@Component({
  selector: 'conditional-text',
  template:
    'MAIN(<ng-template manual>FIRST(<ng-template manual>SECOND(<ng-content></ng-content>)</ng-template>)</ng-template>)',
  standalone: false,
})
class ConditionalTextComponent {}

@Component({
  selector: 'tab',
  template: '

TAB(<ng-content></ng-content>)

',
  standalone: false,
})
class Tab {}

@Component({
  selector: 'tree2',
  inputs: ['depth'],
  template: 'TREE2({{depth}}:<tree *manual [depth]="depth+1"></tree>)',
  standalone: false,
})
class Tree2 {
  depth = 0;
}


describe('enforce no new changes', () => {
      it('should throw when a record gets changed after it has been checked', fakeAsync(() => {
        @Directive({
          selector: '[changed]',
          standalone: false,
        })
        class ChangingDirective {
          @Input() changed: any;
        }

        TestBed.configureTestingModule({declarations: [ChangingDirective]});

        const ctx = createCompFixture('

', TestData);

        ctx.componentInstance.b = 1;
        const errMsgRegExp = /Previous value: 'undefined'\. Current value: '1'/g;
        expect(() => ctx.checkNoChanges()).toThrowError(errMsgRegExp);
      }));

      it('should throw when a record gets changed after the first change detection pass', fakeAsync(() => {
        @Directive({
          selector: '[changed]',
          standalone: false,
        })
        class ChangingDirective {
          @Input() changed: any;
        }

        TestBed.configureTestingModule({declarations: [ChangingDirective]});

        const ctx = createCompFixture('

', TestData);

        ctx.componentInstance.b = 1;
        ctx.detectChanges();

        ctx.componentInstance.b = 2;
        const errMsgRegExp = /Previous value: '1'\. Current value: '2'/g;
        expect(() => ctx.checkNoChanges()).toThrowError(errMsgRegExp);
      }));

      it('should allow view to be created in a cd hook', () => {
        const ctx = createCompFixture('
{{ a }}
', TestData);
        ctx.componentInstance.a = 1;
        ctx.detectChanges();
        expect(ctx.nativeElement.innerText).toEqual('1');
      });

      it('should not throw when two arrays are structurally the same', fakeAsync(() => {
        const ctx = _bindSimpleValue('a', TestData);
        ctx.componentInstance.a = ['value'];
        ctx.detectChanges(false);
        ctx.componentInstance.a = ['value'];
        expect(() => ctx.checkNoChanges()).not.toThrow();
      }));

      it('should not break the next run', fakeAsync(() => {
        const ctx = _bindSimpleValue('a', TestData);
        ctx.componentInstance.a = 'value';
        expect(() => ctx.checkNoChanges()).toThrow();

        ctx.detectChanges();
        expect(renderLog.loggedValues).toEqual(['value']);
      }));

      it('should not break the next run (view engine and ivy)', fakeAsync(() => {
        const ctx = _bindSimpleValue('a', TestData);

        ctx.detectChanges();
        renderLog.clear();

        ctx.componentInstance.a = 'value';
        expect(() => ctx.checkNoChanges()).toThrow();

        ctx.detectChanges();
        expect(renderLog.loggedValues).toEqual(['value']);
      }));
    });

    describe('mode', () => {
      it('Detached', fakeAsync(() => {
        const ctx = createCompFixture('<comp-with-ref></comp-with-ref>');
        const cmp: CompWithRef = queryDirs(ctx.debugElement, CompWithRef)[0];
        cmp.value = 'hello';
        cmp.changeDetectorRef.detach();

        ctx.detectChanges();

        expect(renderLog.log).toEqual([]);
      }));

      it('Detached should disable OnPush', fakeAsync(() => {
        const ctx = createCompFixture('<push-cmp [value]="value"></push-cmp>');
        ctx.componentInstance.value = 0;
        ctx.detectChanges();
        renderLog.clear();

        const cmp: CompWithRef = queryDirs(ctx.debugElement, PushComp)[0];
        cmp.changeDetectorRef.detach();

        ctx.componentInstance.value = 1;
        ctx.detectChanges();

        expect(renderLog.log).toEqual([]);
      }));

      it('Detached view can be checked locally', fakeAsync(() => {
        const ctx = createCompFixture('<wrap-comp-with-ref></wrap-comp-with-ref>');
        const cmp: CompWithRef = queryDirs(ctx.debugElement, CompWithRef)[0];
        cmp.value = 'hello';
        cmp.changeDetectorRef.detach();
        expect(renderLog.log).toEqual([]);

        ctx.detectChanges();

        expect(renderLog.log).toEqual([]);

        cmp.changeDetectorRef.detectChanges();

        expect(renderLog.log).toEqual(['{{hello}}']);
      }));

      it('Reattaches', fakeAsync(() => {
        const ctx = createCompFixture('<comp-with-ref></comp-with-ref>');
        const cmp: CompWithRef = queryDirs(ctx.debugElement, CompWithRef)[0];

        cmp.value = 'hello';
        cmp.changeDetectorRef.detach();

        ctx.detectChanges();

        expect(renderLog.log).toEqual([]);

        cmp.changeDetectorRef.reattach();

        ctx.detectChanges();

        expect(renderLog.log).toEqual(['{{hello}}']);
      }));

      it('Reattaches in the original cd mode', fakeAsync(() => {
        const ctx = createCompFixture('<push-cmp></push-cmp>');
        const cmp: PushComp = queryDirs(ctx.debugElement, PushComp)[0];
        cmp.changeDetectorRef.detach();
        cmp.changeDetectorRef.reattach();

        // renderCount should NOT be incremented with each CD as CD mode
        // should be resetted to
        // on-push
        ctx.detectChanges();
        expect(cmp.renderCount).toBeGreaterThan(0);
        const count = cmp.renderCount;

        ctx.detectChanges();
        expect(cmp.renderCount).toBe(count);
      }));
    });


/**
 * @license
 * Copyright Google LLC All Rights Reserved.
 *
 * Use of this source code is governed by an MIT-style license that can be
 * found in the LICENSE file at https://angular.dev/license
 */

import {computed, signal} from '@angular/core';
import {ReactiveNode, setPostSignalSetFn, SIGNAL} from '@angular/core/primitives/signals';

describe('signals', () => {
  it('should be a getter which reflects the set value', () => {
    const state = signal(false);
    expect(state()).toBeFalse();
    state.set(true);
    expect(state()).toBeTrue();
  });

  it('should accept update function to set new value based on the previous one', () => {
    const counter = signal(0);
    expect(counter()).toEqual(0);

    counter.update((c) => c + 1);
    expect(counter()).toEqual(1);
  });

  it('should not update signal when new value is equal to the previous one', () => {
    const state = signal('aaa', {equal: (a, b) => a.length === b.length});
    expect(state()).toEqual('aaa');

    // set to a "different" value that is "equal" to the previous one
    // there should be no change in the signal's value as the new value is determined to be equal
    // to the previous one
    state.set('bbb');
    expect(state()).toEqual('aaa');

    state.update((_) => 'ccc');
    expect(state()).toEqual('aaa');

    // setting a "non-equal" value
    state.set('d');
    expect(state()).toEqual('d');
  });

  it('should not propagate change when the new signal value is equal to the previous one', () => {
    const state = signal('aaa', {equal: (a, b) => a.length === b.length});
    const upper = computed(() => state().toUpperCase());

    // set to a "different" value that is "equal" to the previous one
    // there should be no change in the signal's value as the new value is determined to be equal
    // to the previous one
    state.set('bbb');
    expect(upper()).toEqual('AAA');

    state.update((_) => 'ccc');
    expect(upper()).toEqual('AAA');

    // setting a "non-equal" value
    state.set('d');
    expect(upper()).toEqual('D');
  });

  it('should consider objects as equal based on their identity with the default equality function', () => {
    let stateValue: unknown = {};
    const state = signal(stateValue);
    let computeCount = 0;
    const derived = computed(() => `${typeof state()}:${++computeCount}`);
    expect(derived()).toEqual('object:1');

    // reset signal value to the same object instance, expect NO change notification
    state.set(stateValue);
    expect(derived()).toEqual('object:1');

    // reset signal value to a different object instance, expect change notification
    stateValue = {};
    state.set(stateValue);
    expect(derived()).toEqual('object:2');

    // reset signal value to a different object type, expect change notification
    stateValue = [];
    state.set(stateValue);
    expect(derived()).toEqual('object:3');

    // reset signal value to the same array instance, expect NO change notification
    state.set(stateValue);
    expect(derived()).toEqual('object:3');
  });

  it('should invoke custom equality function even if old / new references are the same', () => {
    const state = {value: 0};
    const stateSignal = signal(state, {equal: (a, b) => false});

    let computeCount = 0;
    const derived = computed(() => `${stateSignal().value}:${++computeCount}`);

    // derived is re-computed initially
    expect(derived()).toBe('0:1');

    // setting signal with the same reference should propagate change due to the custom equality
    stateSignal.set(state);
    expect(derived()).toBe('0:2');

    // updating signal with the same reference should propagate change as well
    stateSignal.update((state) => state);
    expect(derived()).toBe('0:3');
  });

  it('should allow converting writable signals to their readonly counterpart', () => {
    const counter = signal(0);
    const readOnlyCounter = counter.asReadonly();

    // @ts-expect-error
    expect(readOnlyCounter.set).toBeUndefined();
    // @ts-expect-error
    expect(readOnlyCounter.update).toBeUndefined();

    const double = computed(() => readOnlyCounter() * 2);
    expect(double()).toBe(0);

    counter.set(2);
    expect(double()).toBe(4);
  });

  it('should have a toString implementation', () => {
    const state = signal(false);
    expect(state + '').toBe('[Signal: false]');
  });

  it('should set debugName when a debugName is provided', () => {
    const node = signal(false, {debugName: 'falseSignal'})[SIGNAL] as ReactiveNode;
    expect(node.debugName).toBe('falseSignal');
  });

  describe('optimizations', () => {
    it('should not repeatedly poll status of a non-live node if no signals have changed', () => {
      const unrelated = signal(0);
      const source = signal(1);
      let computations = 0;
      const derived = computed(() => {
        computations++;
        return source() * 2;
      });

      expect(derived()).toBe(2);
      expect(computations).toBe(1);

      const sourceNode = source[SIGNAL] as ReactiveNode;
      // Forcibly increment the version of the source signal. This will cause a mismatch during
      // polling, and will force the derived signal to recompute if polled (which we should observe
      // in this test).
      sourceNode.version++;

      // Read the derived signal again. This should not recompute (even with the forced version
      // update) as no signals have been set since the last read.
      expect(derived()).toBe(2);
      expect(computations).toBe(1);

      // Set the `unrelated` signal, which now means that `derived` should poll if read again.
      // Because of the forced version, that poll will cause a recomputation which we will observe.
      unrelated.set(1);

      expect(derived()).toBe(2);
      expect(computations).toBe(2);
    });
  });

  describe('post-signal-set functions', () => {
    let prevPostSignalSetFn: (() => void) | null = null;
    let log: number;
    beforeEach(() => {
      log = 0;
      prevPostSignalSetFn = setPostSignalSetFn(() => log++);
    });

    afterEach(() => {
      setPostSignalSetFn(prevPostSignalSetFn);
    });

    it('should call the post-signal-set fn when invoking .set()', () => {
      const counter = signal(0);

      counter.set(1);
      expect(log).toBe(1);
    });

    it('should call the post-signal-set fn when invoking .update()', () => {
      const counter = signal(0);

      counter.update((c) => c + 2);
      expect(log).toBe(1);
    });

    it("should not call the post-signal-set fn when the value doesn't change", () => {
      const counter = signal(0);

      counter.set(0);
      expect(log).toBe(0);
    });
  });
});