describe('ngOnChanges', () => {
    it('should invoke ngOnChanges when an input is set on a host directive', () => {
      let firstDirChangeEvent: SimpleChanges | undefined;
      let secondDirChangeEvent: SimpleChanges | undefined;

      @Directive({standalone: true})
      class FirstHostDir implements OnChanges {
        @Input() color?: string;

        ngOnChanges(changes: SimpleChanges) {
          firstDirChangeEvent = changes;
        }
      }

      @Directive({standalone: true})
      class SecondHostDir implements OnChanges {
        @Input() color?: string;

        ngOnChanges(changes: SimpleChanges) {
          secondDirChangeEvent = changes;
        }
      }

      @Directive({
        selector: '[dir]',
        hostDirectives: [
          {directive: FirstHostDir, inputs: ['color']},
          {directive: SecondHostDir, inputs: ['color']},
        ],
        standalone: false,
      })
      class Dir {}

      @Component({
        template: '<button dir [color]="color"></button>',
        standalone: false,
      })
      class App {
        color = 'red';
      }

      TestBed.configureTestingModule({declarations: [App, Dir]});
      const fixture = TestBed.createComponent(App);
      fixture.detectChanges();

      expect(firstDirChangeEvent).toEqual(
        jasmine.objectContaining({
          color: jasmine.objectContaining({
            firstChange: true,
            previousValue: undefined,
            currentValue: 'red',
          }),
        }),
      );

      expect(secondDirChangeEvent).toEqual(
        jasmine.objectContaining({
          color: jasmine.objectContaining({
            firstChange: true,
            previousValue: undefined,
            currentValue: 'red',
          }),
        }),
      );

      fixture.componentInstance.color = 'green';
      fixture.detectChanges();

      expect(firstDirChangeEvent).toEqual(
        jasmine.objectContaining({
          color: jasmine.objectContaining({
            firstChange: false,
            previousValue: 'red',
            currentValue: 'green',
          }),
        }),
      );
      expect(secondDirChangeEvent).toEqual(
        jasmine.objectContaining({
          color: jasmine.objectContaining({
            firstChange: false,
            previousValue: 'red',
            currentValue: 'green',
          }),
        }),
      );
    });

    it('should invoke ngOnChanges when an aliased property is set on a host directive', () => {
      let firstDirChangeEvent: SimpleChanges | undefined;
      let secondDirChangeEvent: SimpleChanges | undefined;

      @Directive({standalone: true})
      class FirstHostDir implements OnChanges {
        @Input('firstAlias') color?: string;

        ngOnChanges(changes: SimpleChanges) {
          firstDirChangeEvent = changes;
        }
      }

      @Directive({standalone: true})
      class SecondHostDir implements OnChanges {
        @Input('secondAlias') color?: string;

        ngOnChanges(changes: SimpleChanges) {
          secondDirChangeEvent = changes;
        }
      }

      @Directive({
        selector: '[dir]',
        hostDirectives: [
          {directive: FirstHostDir, inputs: ['firstAlias: buttonColor']},
          {directive: SecondHostDir, inputs: ['secondAlias: buttonColor']},
        ],
        standalone: false,
      })
      class Dir {}

      @Component({
        template: '<button dir [buttonColor]="color"></button>',
        standalone: false,
      })
      class App {
        color = 'red';
      }

      TestBed.configureTestingModule({declarations: [App, Dir]});
      const fixture = TestBed.createComponent(App);
      fixture.detectChanges();

      expect(firstDirChangeEvent).toEqual(
        jasmine.objectContaining({
          color: jasmine.objectContaining({
            firstChange: true,
            previousValue: undefined,
            currentValue: 'red',
          }),
        }),
      );

      expect(secondDirChangeEvent).toEqual(
        jasmine.objectContaining({
          color: jasmine.objectContaining({
            firstChange: true,
            previousValue: undefined,
            currentValue: 'red',
          }),
        }),
      );

      fixture.componentInstance.color = 'green';
      fixture.detectChanges();

      expect(firstDirChangeEvent).toEqual(
        jasmine.objectContaining({
          color: jasmine.objectContaining({
            firstChange: false,
            previousValue: 'red',
            currentValue: 'green',
          }),
        }),
      );
      expect(secondDirChangeEvent).toEqual(
        jasmine.objectContaining({
          color: jasmine.objectContaining({
            firstChange: false,
            previousValue: 'red',
            currentValue: 'green',
          }),
        }),
      );
    });

    it('should only invoke ngOnChanges on the directives that have exposed an input', () => {
      let firstDirChangeEvent: SimpleChanges | undefined;
      let secondDirChangeEvent: SimpleChanges | undefined;

      @Directive({standalone: true})
      class FirstHostDir implements OnChanges {
        @Input() color?: string;

        ngOnChanges(changes: SimpleChanges) {
          firstDirChangeEvent = changes;
        }
      }

      @Directive({standalone: true})
      class SecondHostDir implements OnChanges {
        @Input() color?: string;

        ngOnChanges(changes: SimpleChanges) {
          secondDirChangeEvent = changes;
        }
      }

      @Directive({
        selector: '[dir]',
        hostDirectives: [FirstHostDir, {directive: SecondHostDir, inputs: ['color']}],
        standalone: false,
      })
      class Dir {}

      @Component({
        template: '<button dir [color]="color"></button>',
        standalone: false,
      })
      class App {
        color = 'red';
      }

      TestBed.configureTestingModule({declarations: [App, Dir]});
      const fixture = TestBed.createComponent(App);
      fixture.detectChanges();

      expect(firstDirChangeEvent).toBe(undefined);
      expect(secondDirChangeEvent).toEqual(
        jasmine.objectContaining({
          color: jasmine.objectContaining({
            firstChange: true,
            previousValue: undefined,
            currentValue: 'red',
          }),
        }),
      );

      fixture.componentInstance.color = 'green';
      fixture.detectChanges();

      expect(firstDirChangeEvent).toBe(undefined);
      expect(secondDirChangeEvent).toEqual(
        jasmine.objectContaining({
          color: jasmine.objectContaining({
            firstChange: false,
            previousValue: 'red',
            currentValue: 'green',
          }),
        }),
      );
    });

    it('should invoke ngOnChanges when a static aliased host directive input is set', () => {
      let latestChangeEvent: SimpleChanges | undefined;

      @Directive({standalone: true})
      class HostDir implements OnChanges {
        @Input('colorAlias') color?: string;

        ngOnChanges(changes: SimpleChanges) {
          latestChangeEvent = changes;
        }
      }

      @Directive({
        selector: '[dir]',
        hostDirectives: [{directive: HostDir, inputs: ['colorAlias: buttonColor']}],
        standalone: false,
      })
      class Dir {}

      @Component({
        template: '<button dir buttonColor="red"></button>',
        standalone: false,
      })
      class App {}

      TestBed.configureTestingModule({declarations: [App, Dir]});
      const fixture = TestBed.createComponent(App);
      fixture.detectChanges();

      expect(latestChangeEvent).toEqual(
        jasmine.objectContaining({
          color: jasmine.objectContaining({
            firstChange: true,
            previousValue: undefined,
            currentValue: 'red',
          }),
        }),
      );
    });
  });


/**
 * @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 {Component, Directive} from '@angular/core';
import {TestBed} from '@angular/core/testing';
import {By} from '@angular/platform-browser';

describe('@angular/common integration', () => {
  describe('NgForOf', () => {
    @Directive({
      selector: '[dir]',
      standalone: false,
    })
    class MyDirective {}

    @Component({
      selector: 'app-child',
      template: '
comp text
',
      standalone: false,
    })
    class ChildComponent {}

    @Component({
      selector: 'app-root',
      template: '',
      standalone: false,
    })
    class AppComponent {
      items: string[] = ['first', 'second'];
    }

    beforeEach(() => {
      TestBed.configureTestingModule({declarations: [AppComponent, ChildComponent, MyDirective]});
    });

    it('should update a loop', () => {
      TestBed.overrideTemplate(
        AppComponent,
        '

{{item}}

',
      );
      const fixture = TestBed.createComponent(AppComponent);
      fixture.detectChanges();

      let listItems = Array.from(
        (fixture.nativeElement as HTMLUListElement).querySelectorAll('li'),
      );
      expect(listItems.map((li) => li.textContent)).toEqual(['first', 'second']);

      // change detection cycle, no model changes
      fixture.detectChanges();
      listItems = Array.from((fixture.nativeElement as HTMLUListElement).querySelectorAll('li'));
      expect(listItems.map((li) => li.textContent)).toEqual(['first', 'second']);

      // remove the last item
      const items = fixture.componentInstance.items;
      items.length = 1;
      fixture.detectChanges();

      listItems = Array.from((fixture.nativeElement as HTMLUListElement).querySelectorAll('li'));
      expect(listItems.map((li) => li.textContent)).toEqual(['first']);

      // change an item
      items[0] = 'one';
      fixture.detectChanges();
      listItems = Array.from((fixture.nativeElement as HTMLUListElement).querySelectorAll('li'));
      expect(listItems.map((li) => li.textContent)).toEqual(['one']);

      // add an item
      items.push('two');
      fixture.detectChanges();
      listItems = Array.from((fixture.nativeElement as HTMLUListElement).querySelectorAll('li'));
      expect(listItems.map((li) => li.textContent)).toEqual(['one', 'two']);
    });

    it('should support ngForOf context variables', () => {
      TestBed.overrideTemplate(
        AppComponent,
        '

{{myIndex}} of {{myCount}}: {{item}}

',
      );
      const fixture = TestBed.createComponent(AppComponent);
      fixture.detectChanges();

      let listItems = Array.from(
        (fixture.nativeElement as HTMLUListElement).querySelectorAll('li'),
      );
      expect(listItems.map((li) => li.textContent)).toEqual(['0 of 2: first', '1 of 2: second']);

      // add an item in the middle
      const items = fixture.componentInstance.items;
      items.splice(1, 0, 'middle');
      fixture.detectChanges();
      listItems = Array.from((fixture.nativeElement as HTMLUListElement).querySelectorAll('li'));
      expect(listItems.map((li) => li.textContent)).toEqual([
        '0 of 3: first',
        '1 of 3: middle',
        '2 of 3: second',
      ]);
    });

    it('should instantiate directives inside directives properly in an ngFor', () => {
      TestBed.overrideTemplate(AppComponent, '<app-child *ngFor="let item of items"></app-child>');
      const fixture = TestBed.createComponent(AppComponent);
      fixture.detectChanges();

      const children = fixture.debugElement.queryAll(By.directive(ChildComponent));

      // expect 2 children, each one with a directive
      expect(children.length).toBe(2);
      expect(children.map((child) => child.nativeElement.innerHTML)).toEqual([
        '
comp text
',
        '
comp text
',
      ]);
      let directive = children[0].query(By.directive(MyDirective));
      expect(directive).not.toBeNull();
      directive = children[1].query(By.directive(MyDirective));
      expect(directive).not.toBeNull();

      // add an item
      const items = fixture.componentInstance.items;
      items.push('third');
      fixture.detectChanges();

      const childrenAfterAdd = fixture.debugElement.queryAll(By.directive(ChildComponent));

      expect(childrenAfterAdd.length).toBe(3);
      expect(childrenAfterAdd.map((child) => child.nativeElement.innerHTML)).toEqual([
        '
comp text
',
        '
comp text
',
        '
comp text
',
      ]);
      directive = childrenAfterAdd[2].query(By.directive(MyDirective));
      expect(directive).not.toBeNull();
    });

    it('should retain parent view listeners when the NgFor destroy views', () => {
      @Component({
        selector: 'app-toggle',
        template: `<button (click)="toggle()">Toggle List</button>

{{item}}

`,
        standalone: false,
      })
      class ToggleComponent {
        private _data: number[] = [1, 2, 3];
        items: number[] = [];

        toggle() {
          if (this.items.length) {
            this.items = [];
          } else {
            this.items = this._data;
          }
        }
      }

      TestBed.configureTestingModule({declarations: [ToggleComponent]});
      const fixture = TestBed.createComponent(ToggleComponent);
      fixture.detectChanges();

      // no elements in the list
      let listItems = Array.from(
        (fixture.nativeElement as HTMLUListElement).querySelectorAll('li'),
      );
      expect(listItems.length).toBe(0);

      // this will fill the list
      fixture.componentInstance.toggle();
      fixture.detectChanges();
      listItems = Array.from((fixture.nativeElement as HTMLUListElement).querySelectorAll('li'));
      expect(listItems.length).toBe(3);
      expect(listItems.map((li) => li.textContent)).toEqual(['1', '2', '3']);

      // now toggle via the button
      const button: HTMLButtonElement = fixture.nativeElement.querySelector('button');
      button.click();
      fixture.detectChanges();
      listItems = Array.from((fixture.nativeElement as HTMLUListElement).querySelectorAll('li'));
      expect(listItems.length).toBe(0);

      // toggle again
      button.click();
      fixture.detectChanges();
      listItems = Array.from((fixture.nativeElement as HTMLUListElement).querySelectorAll('li'));
      expect(listItems.length).toBe(3);
    });


it('should support context for 9+ levels of embedded templates', () => {
      @Component({
        selector: 'app-multi',
        template: `

                                       {{ item8 }}.{{ item7.value }}.{{ item6.value }}.{{ item5.value }}.{{ item4.value }}.{{ item3.value }}.{{ item2.value }}.{{ item1.value }}.{{ item0.value }}.{{ value }}

`,
        standalone: false,
      })
      class NineLevelsComponent {
        value = 'App';
        items: any[] = [
          {
            // item0
            data: [
              {
                // item1
                data: [
                  {
                    // item2
                    data: [
                      {
                        // item3
                        data: [
                          {
                            // item4
                            data: [
                              {
                                // item5
                                data: [
                                  {
                                    // item6
                                    data: [
                                      {
                                        // item7
                                        data: [
                                          '1',
                                          '2', // item8
                                        ],
                                        value: 'h',
                                      },
                                    ],
                                    value: 'g',
                                  },
                                ],
                                value: 'f',
                              },
                            ],
                            value: 'e',
                          },
                        ],
                        value: 'd',
                      },
                    ],
                    value: 'c',
                  },
                ],
                value: 'b',
              },
            ],
            value: 'a',
          },
          {
            // item0
            data: [
              {
                // item1
                data: [
                  {
                    // item2
                    data: [
                      {
                        // item3
                        data: [
                          {
                            // item4
                            data: [
                              {
                                // item5
                                data: [
                                  {
                                    // item6
                                    data: [
                                      {
                                        // item7
                                        data: [
                                          '3',
                                          '4', // item8
                                        ],
                                        value: 'H',
                                      },
                                    ],
                                    value: 'G',
                                  },
                                ],
                                value: 'F',
                              },
                            ],
                            value: 'E',
                          },
                        ],
                        value: 'D',
                      },
                    ],
                    value: 'C',
                  },
                ],
                value: 'B',
              },
            ],
            value: 'A',
          },
        ];
      }

      TestBed.configureTestingModule({declarations: [NineLevelsComponent]});
      const fixture = TestBed.createComponent(NineLevelsComponent);
      fixture.detectChanges();

      const divItems = (fixture.nativeElement as HTMLElement).querySelectorAll('div');
      expect(divItems.length).toBe(2); // 2 outer loops
      let spanItems = divItems[0].querySelectorAll(
        'span > span > span > span > span > span > span > span',
      );
      expect(spanItems.length).toBe(2); // 2 inner elements
      expect(spanItems[0].textContent).toBe('1.h.g.f.e.d.c.b.a.App');
      expect(spanItems[1].textContent).toBe('2.h.g.f.e.d.c.b.a.App');
      spanItems = divItems[1].querySelectorAll(
        'span > span > span > span > span > span > span > span',
      );
      expect(spanItems.length).toBe(2); // 2 inner elements
      expect(spanItems[0].textContent).toBe('3.H.G.F.E.D.C.B.A.App');
      expect(spanItems[1].textContent).toBe('4.H.G.F.E.D.C.B.A.App');
    });
  });

  describe('ngIf', () => {
    it('should support sibling ngIfs', () => {
      @Component({
        selector: 'app-multi',
        template: `

{{ valueOne }}

{{ valueTwo }}

        `,
        standalone: false,
      })
      class SimpleConditionComponent {
        showing = true;
        valueOne = 'one';
        valueTwo = 'two';
      }

      TestBed.configureTestingModule({declarations: [SimpleConditionComponent]});
      const fixture = TestBed.createComponent(SimpleConditionComponent);
      fixture.detectChanges();

      const elements = fixture.nativeElement.querySelectorAll('div');
      expect(elements.length).toBe(2);
      expect(elements[0].textContent).toBe('one');
      expect(elements[1].textContent).toBe('two');

      fixture.componentInstance.valueOne = '$$one$$';
      fixture.componentInstance.valueTwo = '$$two$$';
      fixture.detectChanges();
      expect(elements[0].textContent).toBe('$$one$$');
      expect(elements[1].textContent).toBe('$$two$$');
    });

    it('should handle nested ngIfs with no intermediate context vars', () => {
      @Component({
        selector: 'app-multi',
        template: `

{{ name }}

        `,
        standalone: false,
      })
      class NestedConditionsComponent {
        showing = true;
        outerShowing = true;
        innerShowing = true;
        name = 'App name';
      }

      TestBed.configureTestingModule({declarations: [NestedConditionsComponent]});
      const fixture = TestBed.createComponent(NestedConditionsComponent);
      fixture.detectChanges();

      const elements = fixture.nativeElement.querySelectorAll('div');
      expect(elements.length).toBe(3);
      expect(elements[2].textContent).toBe('App name');

      fixture.componentInstance.name = 'Other name';
      fixture.detectChanges();
      expect(elements[2].textContent).toBe('Other name');
    });
  });

  describe('NgTemplateOutlet', () => {
    it('should create and remove embedded views', () => {
      @Component({
        selector: 'app-multi',
        template: `<ng-template #tpl>from tpl</ng-template>
          <ng-template [ngTemplateOutlet]="showing ? tpl : null"></ng-template>
        `,
        standalone: false,
      })
      class EmbeddedViewsComponent {
        showing = false;
      }

      TestBed.configureTestingModule({declarations: [EmbeddedViewsComponent]});
      const fixture = TestBed.createComponent(EmbeddedViewsComponent);
      fixture.detectChanges();

      expect(fixture.nativeElement.textContent).not.toBe('from tpl');

      fixture.componentInstance.showing = true;
      fixture.detectChanges();
      expect(fixture.nativeElement.textContent).toBe('from tpl');

      fixture.componentInstance.showing = false;
      fixture.detectChanges();
      expect(fixture.nativeElement.textContent).not.toBe('from tpl');
    });

    it('should create and remove embedded views', () => {
      @Component({
        selector: 'app-multi',
        template: `<ng-template #tpl>from tpl</ng-template>
          <ng-container [ngTemplateOutlet]="showing ? tpl : null"></ng-container>
        `,
        standalone: false,
      })
      class NgContainerComponent {
        showing = false;
      }

      TestBed.configureTestingModule({declarations: [NgContainerComponent]});
      const fixture = TestBed.createComponent(NgContainerComponent);
      fixture.detectChanges();

      expect(fixture.nativeElement.textContent).not.toBe('from tpl');

      fixture.componentInstance.showing = true;
      fixture.detectChanges();
      expect(fixture.nativeElement.textContent).toBe('from tpl');

      fixture.componentInstance.showing = false;
      fixture.detectChanges();
      expect(fixture.nativeElement.textContent).not.toBe('from tpl');
    });
  });
});


/**
 * @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 {NgFor} from '@angular/common';
import {
  ChangeDetectorRef,
  Component,
  Directive,
  inject,
  Input,
  OnInit,
  Pipe,
  PipeTransform,
  TemplateRef,
  ViewContainerRef,
} from '@angular/core';
import {TestBed} from '@angular/core/testing';

// Basic shared pipe used during testing.
@Pipe({name: 'multiply', pure: true, standalone: true})
class MultiplyPipe implements PipeTransform {
  transform(value: number, amount: number) {
    return value * amount;
  }
}

describe('control flow - if', () => {
  it('should add and remove views based on conditions change', () => {
    @Component({standalone: true, template: '@if (show) {Something} @else {Nothing}'})
    class TestComponent {
      show = true;
    }

    const fixture = TestBed.createComponent(TestComponent);
    fixture.detectChanges();

    expect(fixture.nativeElement.textContent).toBe('Something');

    fixture.componentInstance.show = false;
    fixture.detectChanges();
    expect(fixture.nativeElement.textContent).toBe('Nothing');
  });

  it('should expose expression value in context', () => {
    @Component({
      standalone: true,
      template: '@if (show; as alias) {{{show}} aliased to {{alias}}}',
    })
    class TestComponent {
      show: any = true;
    }

    const fixture = TestBed.createComponent(TestComponent);
    fixture.detectChanges();
    expect(fixture.nativeElement.textContent).toBe('true aliased to true');

    fixture.componentInstance.show = 1;
    fixture.detectChanges();
    expect(fixture.nativeElement.textContent).toBe('1 aliased to 1');
  });

  it('should not expose the aliased expression to `if` and `else if` blocks', () => {
    @Component({
      standalone: true,
      template: `
          @if (value === 1; as alias) {
            If: {{value}} as {{alias || 'unavailable'}}
          } @else if (value === 2) {
            ElseIf: {{value}} as {{alias || 'unavailable'}}
          } @else {
            Else: {{value}} as {{alias || 'unavailable'}}
          }
        `,
    })
    class TestComponent {
      value = 1;
    }

    const fixture = TestBed.createComponent(TestComponent);
    fixture.detectChanges();
    expect(fixture.nativeElement.textContent.trim()).toBe('If: 1 as true');

    fixture.componentInstance.value = 2;
    fixture.detectChanges();
    expect(fixture.nativeElement.textContent.trim()).toBe('ElseIf: 2 as unavailable');

    fixture.componentInstance.value = 3;
    fixture.detectChanges();
    expect(fixture.nativeElement.textContent.trim()).toBe('Else: 3 as unavailable');
  });

  it('should expose the context to nested conditional blocks', () => {
    @Component({
      standalone: true,
      imports: [MultiplyPipe],
      template: `
          @if (value | multiply:2; as root) {
            Root: {{value}}/{{root}}

            @if (value | multiply:3; as inner) {
              Inner: {{value}}/{{root}}/{{inner}}

              @if (value | multiply:4; as innermost) {
                Innermost: {{value}}/{{root}}/{{inner}}/{{innermost}}
              }
            }
          }
        `,
    })
    class TestComponent {
      value = 1;
    }

    const fixture = TestBed.createComponent(TestComponent);
    fixture.detectChanges();
    let content = fixture.nativeElement.textContent;
    expect(content).toContain('Root: 1/2');
    expect(content).toContain('Inner: 1/2/3');
    expect(content).toContain('Innermost: 1/2/3/4');

    fixture.componentInstance.value = 2;
    fixture.detectChanges();
    content = fixture.nativeElement.textContent;
    expect(content).toContain('Root: 2/4');
    expect(content).toContain('Inner: 2/4/6');
    expect(content).toContain('Innermost: 2/4/6/8');
  });

  it('should expose the context to listeners inside nested conditional blocks', () => {
    let logs: any[] = [];

    @Component({
      standalone: true,
      imports: [MultiplyPipe],
      template: `
          @if (value | multiply:2; as root) {
            <button (click)="log(['Root', value, root])"></button>

            @if (value | multiply:3; as inner) {
              <button (click)="log(['Inner', value, root, inner])"></button>

              @if (value | multiply:4; as innermost) {
                <button (click)="log(['Innermost', value, root, inner, innermost])"></button>
              }
            }
          }
        `,
    })
    class TestComponent {
      value = 1;

      log(value: any) {
        logs.push(value);
      }
    }

    const fixture = TestBed.createComponent(TestComponent);
    fixture.detectChanges();
    const buttons = Array.from<HTMLButtonElement>(fixture.nativeElement.querySelectorAll('button'));
    buttons.forEach((button) => button.click());
    fixture.detectChanges();

    expect(logs).toEqual([
      ['Root', 1, 2],
      ['Inner', 1, 2, 3],
      ['Innermost', 1, 2, 3, 4],
    ]);

    logs = [];
    fixture.componentInstance.value = 2;
    fixture.detectChanges();

    buttons.forEach((button) => button.click());
    fixture.detectChanges();
    expect(logs).toEqual([
      ['Root', 2, 4],
      ['Inner', 2, 4, 6],
      ['Innermost', 2, 4, 6, 8],
    ]);
  });

  it('should expose expression value passed through a pipe in context', () => {
    @Component({
      standalone: true,
      template: '@if (value | multiply:2; as alias) {{{value}} aliased to {{alias}}}',
      imports: [MultiplyPipe],
    })
    class TestComponent {
      value = 1;
    }

    const fixture = TestBed.createComponent(TestComponent);
    fixture.detectChanges();
    expect(fixture.nativeElement.textContent).toBe('1 aliased to 2');

    fixture.componentInstance.value = 4;
    fixture.detectChanges();
    expect(fixture.nativeElement.textContent).toBe('4 aliased to 8');
  });

  it('should destroy all views if there is nothing to display', () => {
    @Component({
      standalone: true,
      template: '@if (show) {Something}',
    })
    class TestComponent {
      show = true;
    }

    const fixture = TestBed.createComponent(TestComponent);
    fixture.detectChanges();
    expect(fixture.nativeElement.textContent).toBe('Something');

    fixture.componentInstance.show = false;
    fixture.detectChanges();
    expect(fixture.nativeElement.textContent).toBe('');
  });

  it('should be able to use pipes in conditional expressions', () => {
    @Component({
      standalone: true,
      imports: [MultiplyPipe],
      template: `
          @if ((value | multiply:2) === 2) {
            one
          } @else if ((value | multiply:2) === 4) {
            two
          } @else {
            nothing
          }
        `,
    })
    class TestComponent {
      value = 0;
    }

    const fixture = TestBed.createComponent(TestComponent);
    fixture.detectChanges();

    expect(fixture.nativeElement.textContent.trim()).toBe('nothing');

    fixture.componentInstance.value = 2;
    fixture.detectChanges();
    expect(fixture.nativeElement.textContent.trim()).toBe('two');

    fixture.componentInstance.value = 1;
    fixture.detectChanges();
    expect(fixture.nativeElement.textContent.trim()).toBe('one');
  });

  it('should be able to use pipes injecting ChangeDetectorRef in if blocks', () => {
    @Pipe({name: 'test', standalone: true})
    class TestPipe implements PipeTransform {
      changeDetectorRef = inject(ChangeDetectorRef);

      transform(value: any) {
        return value;
      }
    }

    @Component({
      standalone: true,
      template: '@if (show | test) {Something}',
      imports: [TestPipe],
    })
    class TestComponent {
      show = true;
    }

    const fixture = TestBed.createComponent(TestComponent);
    fixture.detectChanges();
    expect(fixture.nativeElement.textContent).toBe('Something');
  });

  it('should support a condition with the a typeof expression', () => {
    @Component({
      standalone: true,
      template: `
          @if (typeof value === 'string') {
            {{value.length}}
          } @else {
            {{value}}
          }
        `,
    })
    class TestComponent {
      value: string | number = 'string';
    }

    const fixture = TestBed.createComponent(TestComponent);
    fixture.detectChanges();
    expect(fixture.nativeElement.textContent.trim()).toBe('6');

    fixture.componentInstance.value = 42;
    fixture.detectChanges();
    expect(fixture.nativeElement.textContent.trim()).toBe('42');
  });


/**
 * @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 {NgIf} from '@angular/common';
import {
  ChangeDetectorRef,
  Component,
  Directive,
  inject,
  Input,
  OnInit,
  Pipe,
  PipeTransform,
  TemplateRef,
  ViewContainerRef,
} from '@angular/core';
import {TestBed} from '@angular/core/testing';

describe('control flow - for', () => {
  it('should create, remove and move views corresponding to items in a collection', () => {
    @Component({
      template: '@for ((item of items); track item; let idx = $index) {{{item}}({{idx}})|}',
      standalone: false,
    })
    class TestComponent {
      items = [1, 2, 3];
    }

    const fixture = TestBed.createComponent(TestComponent);
    fixture.detectChanges();
    expect(fixture.nativeElement.textContent).toBe('1(0)|2(1)|3(2)|');

    fixture.componentInstance.items.pop();
    fixture.detectChanges();
    expect(fixture.nativeElement.textContent).toBe('1(0)|2(1)|');

    fixture.componentInstance.items.push(3);
    fixture.detectChanges();
    expect(fixture.nativeElement.textContent).toBe('1(0)|2(1)|3(2)|');

    fixture.componentInstance.items[0] = 3;
    fixture.componentInstance.items[2] = 1;
    fixture.detectChanges();
    expect(fixture.nativeElement.textContent).toBe('3(0)|2(1)|1(2)|');
  });

  it('should loop over iterators that can be iterated over only once', () => {
    @Component({
      template: '@for ((item of items.keys()); track $index) {{{item}}|}',
      standalone: false,
    })
    class TestComponent {
      items = new Map([
        ['a', 1],
        ['b', 2],
        ['c', 3],
      ]);
    }

    const fixture = TestBed.createComponent(TestComponent);
    fixture.detectChanges();
    expect(fixture.nativeElement.textContent).toBe('a|b|c|');
  });

  it('should work correctly with trackBy index', () => {
    @Component({
      template: '@for ((item of items); track idx; let idx = $index) {{{item}}({{idx}})|}',
      standalone: false,
    })
    class TestComponent {
      items = [1, 2, 3];
    }

    const fixture = TestBed.createComponent(TestComponent);
    fixture.detectChanges();
    expect(fixture.nativeElement.textContent).toBe('1(0)|2(1)|3(2)|');

    fixture.componentInstance.items.pop();
    fixture.detectChanges();
    expect(fixture.nativeElement.textContent).toBe('1(0)|2(1)|');

    fixture.componentInstance.items.push(3);
    fixture.detectChanges();
    expect(fixture.nativeElement.textContent).toBe('1(0)|2(1)|3(2)|');

    fixture.componentInstance.items[0] = 3;
    fixture.componentInstance.items[2] = 1;
    fixture.detectChanges();
    expect(fixture.nativeElement.textContent).toBe('3(0)|2(1)|1(2)|');
  });

  it('should support empty blocks', () => {
    @Component({
      template: '@for ((item of items); track idx; let idx = $index) {|} @empty {Empty}',
      standalone: false,
    })
    class TestComponent {
      items: number[] | null | undefined = [1, 2, 3];
    }

    const fixture = TestBed.createComponent(TestComponent);
    fixture.detectChanges();
    expect(fixture.nativeElement.textContent).toBe('|||');

    fixture.componentInstance.items = [];
    fixture.detectChanges();
    expect(fixture.nativeElement.textContent).toBe('Empty');

    fixture.componentInstance.items = [0, 1];
    fixture.detectChanges();
    expect(fixture.nativeElement.textContent).toBe('||');

    fixture.componentInstance.items = null;
    fixture.detectChanges();
    expect(fixture.nativeElement.textContent).toBe('Empty');

    fixture.componentInstance.items = [0];
    fixture.detectChanges();
    expect(fixture.nativeElement.textContent).toBe('|');

    fixture.componentInstance.items = undefined;
    fixture.detectChanges();
    expect(fixture.nativeElement.textContent).toBe('Empty');
  });

  it('should be able to use pipes injecting ChangeDetectorRef in for loop blocks', () => {
    @Pipe({name: 'test', standalone: true})
    class TestPipe implements PipeTransform {
      changeDetectorRef = inject(ChangeDetectorRef);

      transform(value: any) {
        return value;
      }
    }

    @Component({
      template: '@for (item of items | test; track item;) {{{item}}|}',
      imports: [TestPipe],
      standalone: true,
    })
    class TestComponent {
      items = [1, 2, 3];
    }

    const fixture = TestBed.createComponent(TestComponent);
    fixture.detectChanges();
    expect(fixture.nativeElement.textContent).toBe('1|2|3|');
  });

  it('should be able to access a directive property that is reassigned in a lifecycle hook', () => {
    @Directive({
      selector: '[dir]',
      exportAs: 'dir',
      standalone: true,
    })
    class Dir {
      data = [1];

      ngDoCheck() {
        this.data = [2];
      }
    }

    @Component({
      selector: 'app-root',
      standalone: true,
      imports: [Dir],
      template: `
        <div [dir] #dir="dir"></div>

        @for (x of dir.data; track $index) {
          {{x}}
        }
      `,
    })
    class TestComponent {}

    const fixture = TestBed.createComponent(TestComponent);
    fixture.detectChanges();
    expect(fixture.nativeElement.textContent.trim()).toBe('2');
  });

  it('should expose variables both under their real names and aliases', () => {
    @Component({
      template:
        '@for ((item of items); track item; let idx = $index) {{{item}}({{$index}}/{{idx}})|}',
      standalone: false,
    })
    class TestComponent {
      items = [1, 2, 3];
    }

    const fixture = TestBed.createComponent(TestComponent);
    fixture.detectChanges();
    expect(fixture.nativeElement.textContent).toBe('1(0/0)|2(1/1)|3(2/2)|');

    fixture.componentInstance.items.splice(1, 1);
    fixture.detectChanges();
    expect(fixture.nativeElement.textContent).toBe('1(0/0)|3(1/1)|');
  });


describe('trackBy', () => {
    it('should have access to the host context in the track function', () => {
      let offsetReads = 0;

      @Component({
        template: '@for ((item of items); track $index + offset) {{{item}}}',
        standalone: false,
      })
      class TestComponent {
        items = ['a', 'b', 'c'];

        get offset() {
          offsetReads++;
          return 0;
        }
      }

      const fixture = TestBed.createComponent(TestComponent);
      fixture.detectChanges();
      expect(fixture.nativeElement.textContent).toBe('abc');
      expect(offsetReads).toBeGreaterThan(0);

      const prevReads = offsetReads;
      // explicitly modify the DOM text node to make sure that the list reconciliation algorithm
      // based on tracking indices overrides it.
      fixture.debugElement.childNodes[1].nativeNode.data = 'x';
      fixture.componentInstance.items.shift();
      fixture.detectChanges();
      expect(fixture.nativeElement.textContent).toBe('bc');
      expect(offsetReads).toBeGreaterThan(prevReads);
    });

    it('should be able to access component properties in the tracking function from a loop at the root of the template', () => {
      const calls = new Set();

      @Component({
        template: `@for ((item of items); track trackingFn(item, compProp)) {{{item}}}`,
        standalone: false,
      })
      class TestComponent {
        items = ['a', 'b'];
        compProp = 'hello';

        trackingFn(item: string, message: string) {
          calls.add(`${item}:${message}`);
          return item;
        }
      }

      const fixture = TestBed.createComponent(TestComponent);
      fixture.detectChanges();
      expect([...calls].sort()).toEqual(['a:hello', 'b:hello']);
    });

    it('should be able to access component properties in the tracking function from a nested template', () => {
      const calls = new Set();

      @Component({
        template: `
            @if (true) {
              @if (true) {
                @if (true) {
                  @for ((item of items); track trackingFn(item, compProp)) {{{item}}}
                }
              }
            }
          `,
        standalone: false,
      })
      class TestComponent {
        items = ['a', 'b'];
        compProp = 'hello';

        trackingFn(item: string, message: string) {
          calls.add(`${item}:${message}`);
          return item;
        }
      }

      const fixture = TestBed.createComponent(TestComponent);
      fixture.detectChanges();
      expect([...calls].sort()).toEqual(['a:hello', 'b:hello']);
    });

    it('should invoke method tracking function with the correct context', () => {
      let context = null as TestComponent | null;

      @Component({
        template: `@for (item of items; track trackingFn($index, item)) {{{item}}}`,
        standalone: false,
      })
      class TestComponent {
        items = ['a', 'b'];

        trackingFn(_index: number, item: string) {
          context = this;
          return item;
        }
      }

      const fixture = TestBed.createComponent(TestComponent);
      fixture.detectChanges();
      expect(context).toBe(fixture.componentInstance);
    });

    it('should warn about duplicated keys when using arrays', () => {
      @Component({
        template: `@for (item of items; track item) {{{item}}}`,
        standalone: false,
      })
      class TestComponent {
        items = ['a', 'b', 'a', 'c', 'a'];
      }

      spyOn(console, 'warn');

      const fixture = TestBed.createComponent(TestComponent);
      fixture.detectChanges();
      expect(fixture.nativeElement.textContent).toBe('abaca');
      expect(console.warn).toHaveBeenCalledTimes(1);
      expect(console.warn).toHaveBeenCalledWith(
        jasmine.stringContaining(
          `NG0955: The provided track expression resulted in duplicated keys for a given collection.`,
        ),
      );
      expect(console.warn).toHaveBeenCalledWith(
        jasmine.stringContaining(
          `Adjust the tracking expression such that it uniquely identifies all the items in the collection. `,
        ),
      );
      expect(console.warn).toHaveBeenCalledWith(
        jasmine.stringContaining(`key "a" at index "0" and "2"`),
      );
      expect(console.warn).toHaveBeenCalledWith(
        jasmine.stringContaining(`key "a" at index "2" and "4"`),
      );
    });

    it('should warn about duplicated keys when using iterables', () => {
      @Component({
        template: `@for (item of items.values(); track item) {{{item}}}`,
        standalone: false,
      })
      class TestComponent {
        items = new Map([
          [1, 'a'],
          [2, 'b'],
          [3, 'a'],
          [4, 'c'],
          [5, 'a'],
        ]);
      }

      spyOn(console, 'warn');

      const fixture = TestBed.createComponent(TestComponent);
      fixture.detectChanges();
      expect(fixture.nativeElement.textContent).toBe('abaca');
      expect(console.warn).toHaveBeenCalledTimes(1);
      expect(console.warn).toHaveBeenCalledWith(
        jasmine.stringContaining(
          `NG0955: The provided track expression resulted in duplicated keys for a given collection.`,
        ),
      );
      expect(console.warn).toHaveBeenCalledWith(
        jasmine.stringContaining(
          `Adjust the tracking expression such that it uniquely identifies all the items in the collection. `,
        ),
      );
      expect(console.warn).toHaveBeenCalledWith(
        jasmine.stringContaining(`key "a" at index "0" and "2"`),
      );
      expect(console.warn).toHaveBeenCalledWith(
        jasmine.stringContaining(`key "a" at index "2" and "4"`),
      );
    });

    it('should warn about duplicate keys when keys are expressed as symbols', () => {
      const value = Symbol('a');

      @Component({
        template: `@for (item of items.values(); track item) {}`,
        standalone: false,
      })
      class TestComponent {
        items = new Map([
          [1, value],
          [2, value],
        ]);
      }

      spyOn(console, 'warn');

      const fixture = TestBed.createComponent(TestComponent);
      fixture.detectChanges();
      expect(console.warn).toHaveBeenCalledWith(
        jasmine.stringContaining(`Symbol(a)" at index "0" and "1".`),
      );
    });

    it('should not warn about duplicate keys iterating over the new collection only', () => {
      @Component({
        template: `@for (item of items; track item) {}`,
        standalone: false,
      })
      class TestComponent {
        items = [1, 2, 3];
      }

      spyOn(console, 'warn');

      const fixture = TestBed.createComponent(TestComponent);
      fixture.detectChanges();
      expect(console.warn).not.toHaveBeenCalled();

      fixture.componentInstance.items = [4, 5, 6];
      fixture.detectChanges();
      expect(console.warn).not.toHaveBeenCalled();
    });

    it('should warn about collection re-creation due to identity tracking', () => {
      @Component({
        template: `@for (item of items; track item) {(<span>{{item.value}}</span>)}`,
        standalone: false,
      })
      class TestComponent {
        items = [{value: 0}, {value: 1}];
      }

      spyOn(console, 'warn');

      const fixture = TestBed.createComponent(TestComponent);
      fixture.detectChanges();
      expect(fixture.nativeElement.textContent).toBe('(0)(1)');
      expect(console.warn).not.toHaveBeenCalled();

      fixture.componentInstance.items = fixture.componentInstance.items.map((item) => ({
        value: item.value + 1,
      }));
      fixture.detectChanges();
      expect(fixture.nativeElement.textContent).toBe('(1)(2)');
      expect(console.warn).toHaveBeenCalled();
    });

    it('should NOT warn about collection re-creation when a view is not considered expensive', () => {
      @Component({
        template: `@for (item of items; track item) {({{item.value}})}`,
        standalone: false,
      })
      class TestComponent {
        items = [{value: 0}, {value: 1}];
      }

      spyOn(console, 'warn');

      const fixture = TestBed.createComponent(TestComponent);
      fixture.detectChanges();
      expect(fixture.nativeElement.textContent).toBe('(0)(1)');
      expect(console.warn).not.toHaveBeenCalled();

      fixture.componentInstance.items = fixture.componentInstance.items.map((item) => ({
        value: item.value + 1,
      }));
      fixture.detectChanges();
      expect(fixture.nativeElement.textContent).toBe('(1)(2)');
      expect(console.warn).not.toHaveBeenCalled();
    });

    it('should NOT warn about collection re-creation when a trackBy function is not identity', () => {
      @Component({
        template: `@for (item of items; track item.value) {({{item.value}})}`,
        standalone: false,
      })
      class TestComponent {
        items = [{value: 0}, {value: 1}];
      }

      spyOn(console, 'warn');

      const fixture = TestBed.createComponent(TestComponent);
      fixture.detectChanges();
      expect(fixture.nativeElement.textContent).toBe('(0)(1)');
      expect(console.warn).not.toHaveBeenCalled();

      fixture.componentInstance.items = fixture.componentInstance.items.map((item) => ({
        value: item.value + 1,
      }));
      fixture.detectChanges();
      expect(fixture.nativeElement.textContent).toBe('(1)(2)');
      expect(console.warn).not.toHaveBeenCalled();
    });
  });


describe('list diffing and view operations', () => {
    it('should delete views in the middle', () => {
      @Component({
        template: '@for (item of items; track item; let idx = $index) {{{item}}({{idx}})|}',
        standalone: false,
      })
      class TestComponent {
        items = [1, 2, 3];
      }

      const fixture = TestBed.createComponent(TestComponent);
      fixture.detectChanges();
      expect(fixture.nativeElement.textContent).toBe('1(0)|2(1)|3(2)|');

      // delete in the middle
      fixture.componentInstance.items.splice(1, 1);
      fixture.detectChanges();
      expect(fixture.nativeElement.textContent).toBe('1(0)|3(1)|');
    });

    it('should insert views in the middle', () => {
      @Component({
        template: '@for (item of items; track item; let idx = $index) {{{item}}({{idx}})|}',
        standalone: false,
      })
      class TestComponent {
        items = [1, 3];
      }

      const fixture = TestBed.createComponent(TestComponent);
      fixture.detectChanges();
      expect(fixture.nativeElement.textContent).toBe('1(0)|3(1)|');

      // add in the middle
      fixture.componentInstance.items.splice(1, 0, 2);
      fixture.detectChanges();
      expect(fixture.nativeElement.textContent).toBe('1(0)|2(1)|3(2)|');
    });

    it('should replace different items', () => {
      @Component({
        template: '@for (item of items; track item; let idx = $index) {{{item}}({{idx}})|}',
        standalone: false,
      })
      class TestComponent {
        items = [1, 2, 3];
      }

      const fixture = TestBed.createComponent(TestComponent);
      fixture.detectChanges();
      expect(fixture.nativeElement.textContent).toBe('1(0)|2(1)|3(2)|');

      // an item in the middle stays the same, the rest gets replaced
      fixture.componentInstance.items = [5, 2, 7];
      fixture.detectChanges();
      expect(fixture.nativeElement.textContent).toBe('5(0)|2(1)|7(2)|');
    });

    it('should move and delete items', () => {
      @Component({
        template: '@for (item of items; track item; let idx = $index) {{{item}}({{idx}})|}',
        standalone: false,
      })
      class TestComponent {
        items = [1, 2, 3, 4, 5, 6];
      }

      const fixture = TestBed.createComponent(TestComponent);
      fixture.detectChanges(false);
      expect(fixture.nativeElement.textContent).toBe('1(0)|2(1)|3(2)|4(3)|5(4)|6(5)|');

      // move 5 and do some other delete other operations
      fixture.componentInstance.items = [5, 3, 7];
      fixture.detectChanges();
      expect(fixture.nativeElement.textContent).toBe('5(0)|3(1)|7(2)|');
    });

    it('should correctly attach and detach views with duplicated keys', () => {
      const BEFORE = [
        {'name': 'Task 14', 'id': 14},
        {'name': 'Task 14', 'id': 14},
        {'name': 'Task 70', 'id': 70},
        {'name': 'Task 34', 'id': 34},
      ];

      const AFTER = [
        {'name': 'Task 70', 'id': 70},
        {'name': 'Task 14', 'id': 14},
        {'name': 'Task 28', 'id': 28},
      ];

      @Component({
        standalone: true,
        template: ``,
        selector: 'child-cmp',
      })
      class ChildCmp {}

      @Component({
        standalone: true,
        imports: [ChildCmp],
        template: `
          @for(task of tasks; track task.id) {
            <child-cmp/>
          }
        `,
      })
      class TestComponent {
        tasks = BEFORE;
      }

      const fixture = TestBed.createComponent(TestComponent);
      fixture.detectChanges();

      const cmp = fixture.componentInstance;
      const nativeElement = fixture.debugElement.nativeElement;
      cmp.tasks = AFTER;
      fixture.detectChanges();
      expect(nativeElement.querySelectorAll('child-cmp').length).toBe(3);
    });
  });


it('should not project the root node across multiple layers of @for', () => {
      @Component({
        standalone: true,
        selector: 'test',
        template: 'Main: <ng-content/> Slot: <ng-content select="[foo]"/>',
      })
      class TestComponent {}

      @Component({
        standalone: true,
        imports: [TestComponent],
        template: `
        <test>Before @for (item of items; track $index) {
          @for (item of items; track $index) {
            {{item}}
          }
        } After</test>
      `,
      })
      class App {
        items = [1, 2];
      }

      const fixture = TestBed.createComponent(App);
      fixture.detectChanges();
      expect(fixture.nativeElement.textContent).toBe('Main: Before 1212 After Slot: ');
    });

    it('should project an @for with a single root template node into the root node slot', () => {
      @Component({
        standalone: true,
        selector: 'test',
        template: 'Main: <ng-content/> Slot: <ng-content select="[foo]"/>',
      })
      class TestComponent {}

      @Component({
        standalone: true,
        imports: [TestComponent, NgIf],
        template: `<test>Before @for (item of items; track $index) {
        {{item}}
      } After</test>`,
      })
      class App {
        items = [1, 2];
      }

      const fixture = TestBed.createComponent(App);
      fixture.detectChanges();
      expect(fixture.nativeElement.textContent).toBe('Main: Before  After Slot: 12');

      fixture.componentInstance.items.push(3);
      fixture.detectChanges();
      expect(fixture.nativeElement.textContent).toBe('Main: Before  After Slot: 123');
    });

    it('should invoke a projected attribute directive at the root of an @for once', () => {
      let directiveCount = 0;

      @Component({
        standalone: true,
        selector: 'test',
        template: 'Main: <ng-content/> Slot: <ng-content select="[foo]"/>',
      })
      class TestComponent {}

      @Directive({
        selector: '[foo]',
        standalone: true,
      })
      class FooDirective {
        constructor() {
          directiveCount++;
        }
      }

      @Component({
        standalone: true,
        imports: [TestComponent, FooDirective],
        template: `<test>Before @for (item of items; track $index) {
        {{item}}
      } After</test>
      `,
      })
      class App {
        items = [1];
      }

      const fixture = TestBed.createComponent(App);
      fixture.detectChanges();

      expect(directiveCount).toBe(1);
      expect(fixture.nativeElement.textContent).toBe('Main: Before  After Slot: 1');
    });

    it('should invoke a projected template directive at the root of an @for once', () => {
      let directiveCount = 0;

      @Component({
        standalone: true,
        selector: 'test',
        template: 'Main: <ng-content/> Slot: <ng-content select="[foo]"/>',
      })
      class TestComponent {}

      @Directive({
        selector: '[templateDir]',
        standalone: true,
      })
      class TemplateDirective implements OnInit {
        constructor(
          private viewContainerRef: ViewContainerRef,
          private templateRef: TemplateRef<any>,
        ) {
          directiveCount++;
        }

        ngOnInit(): void {
          const view = this.viewContainerRef.createEmbeddedView(this.templateRef);
          this.viewContainerRef.insert(view);
        }
      }

      @Component({
        standalone: true,
        imports: [TestComponent, TemplateDirective],
        template: `<test>Before @for (item of items; track $index) {
        {{item}}
      } After</test>
      `,
      })
      class App {
        items = [1];
      }

      const fixture = TestBed.createComponent(App);
      fixture.detectChanges();

      expect(directiveCount).toBe(1);
      expect(fixture.nativeElement.textContent).toBe('Main: Before  After Slot: 1');
    });

    it('should invoke a directive on a projected ng-template at the root of an @for once', () => {
      let directiveCount = 0;

      @Component({
        standalone: true,
        selector: 'test',
        template: 'Main: <ng-content/> Slot: <ng-content select="[foo]"/>',
      })
      class TestComponent {}

      @Directive({
        selector: '[templateDir]',
        standalone: true,
      })
      class TemplateDirective implements OnInit {
        constructor(
          private viewContainerRef: ViewContainerRef,
          private templateRef: TemplateRef<any>,
        ) {
          directiveCount++;
        }

        ngOnInit(): void {
          const view = this.viewContainerRef.createEmbeddedView(this.templateRef);
          this.viewContainerRef.insert(view);
        }
      }

      @Component({
        standalone: true,
        imports: [TestComponent, TemplateDirective],
        template: `<test>Before @for (item of items; track $index) {
        <ng-template templateDir foo>{{item}}</ng-template>
      } After</test>
      `,
      })
      class App {
        items = [1];
      }

      const fixture = TestBed.createComponent(App);
      fixture.detectChanges();

      expect(directiveCount).toBe(1);
      expect(fixture.nativeElement.textContent).toBe('Main: Before  After Slot: 1');
    });
  });
});


/**
 * @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 {CommonModule} from '@angular/common';
import {
  AfterViewInit,
  ChangeDetectorRef,
  Component,
  ContentChildren,
  Directive,
  DoCheck,
  Input,
  NgModule,
  OnChanges,
  QueryList,
  SimpleChange,
  SimpleChanges,
  TemplateRef,
  ViewChild,
  ViewContainerRef,
} from '@angular/core';
import {TestBed} from '@angular/core/testing';
import {By} from '@angular/platform-browser';

describe('onChanges', () => {
  it('should correctly support updating one Input among many', () => {
    let log: string[] = [];

    @Component({
      selector: 'child-comp',
      template: 'child',
      standalone: false,
    })
    class ChildComp implements OnChanges {
      @Input() a: number = 0;
      @Input() b: number = 0;
      @Input() c: number = 0;

      ngOnChanges(changes: SimpleChanges) {
        for (let key in changes) {
          const simpleChange = changes[key];
          log.push(key + ': ' + simpleChange.previousValue + ' -> ' + simpleChange.currentValue);
        }
      }
    }

    @Component({
      selector: 'app-comp',
      template: '<child-comp [a]="a" [b]="b" [c]="c"></child-comp>',
      standalone: false,
    })
    class AppComp {
      a = 0;
      b = 0;
      c = 0;
    }

    TestBed.configureTestingModule({declarations: [AppComp, ChildComp]});
    const fixture = TestBed.createComponent(AppComp);
    fixture.detectChanges();
    const appComp = fixture.componentInstance;
    expect(log).toEqual(['a: undefined -> 0', 'b: undefined -> 0', 'c: undefined -> 0']);
    log.length = 0;

    appComp.a = 1;
    fixture.detectChanges();
    expect(log).toEqual(['a: 0 -> 1']);
    log.length = 0;

    appComp.b = 2;
    fixture.detectChanges();
    expect(log).toEqual(['b: 0 -> 2']);
    log.length = 0;

    appComp.c = 3;
    fixture.detectChanges();
    expect(log).toEqual(['c: 0 -> 3']);
  });

  it('should call onChanges method after inputs are set in creation and update mode', () => {
    const events: any[] = [];

    @Component({
      selector: 'comp',
      template: `<p>test</p>`,
      standalone: false,
    })
    class Comp {
      @Input() val1 = 'a';

      @Input('publicVal2') val2 = 'b';

      ngOnChanges(changes: SimpleChanges) {
        events.push({name: 'comp', changes});
      }
    }

    @Component({
      template: `<comp [val1]="val1" [publicVal2]="val2"></comp>`,
      standalone: false,
    })
    class App {
      val1 = 'a2';

      val2 = 'b2';
    }

    TestBed.configureTestingModule({
      declarations: [App, Comp],
    });
    const fixture = TestBed.createComponent(App);
    fixture.detectChanges();

    expect(events).toEqual([
      {
        name: 'comp',
        changes: {
          val1: new SimpleChange(undefined, 'a2', true),
          val2: new SimpleChange(undefined, 'b2', true),
        },
      },
    ]);

    events.length = 0;
    fixture.componentInstance.val1 = 'a3';
    fixture.componentInstance.val2 = 'b3';
    fixture.detectChanges();

    expect(events).toEqual([
      {
        name: 'comp',
        changes: {
          val1: new SimpleChange('a2', 'a3', false),
          val2: new SimpleChange('b2', 'b3', false),
        },
      },
    ]);
  });

  it('should call parent onChanges before child onChanges', () => {
    const events: any[] = [];

    @Component({
      selector: 'parent',
      template: `<child [val]="val"></child>`,
      standalone: false,
    })
    class Parent {
      @Input() val = '';

      ngOnChanges(changes: SimpleChanges) {
        events.push({name: 'parent', changes});
      }
    }

    @Component({
      selector: 'child',
      template: `<p>test</p>`,
      standalone: false,
    })
    class Child {
      @Input() val = '';

      ngOnChanges(changes: SimpleChanges) {
        events.push({name: 'child', changes});
      }
    }

    @Component({
      template: `<parent [val]="val"></parent>`,
      standalone: false,
    })
    class App {
      val = 'foo';
    }

    TestBed.configureTestingModule({
      declarations: [App, Child, Parent],
    });
    const fixture = TestBed.createComponent(App);
    fixture.detectChanges();

    expect(events).toEqual([
      {
        name: 'parent',
        changes: {
          val: new SimpleChange(undefined, 'foo', true),
        },
      },
      {
        name: 'child',
        changes: {
          val: new SimpleChange(undefined, 'foo', true),
        },
      },
    ]);

    events.length = 0;
    fixture.componentInstance.val = 'bar';
    fixture.detectChanges();

    expect(events).toEqual([
      {
        name: 'parent',
        changes: {
          val: new SimpleChange('foo', 'bar', false),
        },
      },
      {
        name: 'child',
        changes: {
          val: new SimpleChange('foo', 'bar', false),
        },
      },
    ]);
  });

  it('should call all parent onChanges across view before calling children onChanges', () => {
    const events: any[] = [];

    @Component({
      selector: 'parent',
      template: `<child [name]="name" [val]="val"></child>`,
      standalone: false,
    })
    class Parent {
      @Input() val = '';

      @Input() name = '';

      ngOnChanges(changes: SimpleChanges) {
        events.push({name: 'parent ' + this.name, changes});
      }
    }

    @Component({
      selector: 'child',
      template: `<p>test</p>`,
      standalone: false,
    })
    class Child {
      @Input() val = '';

      @Input() name = '';

      ngOnChanges(changes: SimpleChanges) {
        events.push({name: 'child ' + this.name, changes});
      }
    }

    @Component({
      template: `
        <parent name="1" [val]="val"></parent>
        <parent name="2" [val]="val"></parent>
      `,
      standalone: false,
    })
    class App {
      val = 'foo';
    }

    TestBed.configureTestingModule({
      declarations: [App, Child, Parent],
    });
    const fixture = TestBed.createComponent(App);
    fixture.detectChanges();

    expect(events).toEqual([
      {
        name: 'parent 1',
        changes: {
          name: new SimpleChange(undefined, '1', true),
          val: new SimpleChange(undefined, 'foo', true),
        },
      },
      {
        name: 'parent 2',
        changes: {
          name: new SimpleChange(undefined, '2', true),
          val: new SimpleChange(undefined, 'foo', true),
        },
      },
      {
        name: 'child 1',
        changes: {
          name: new SimpleChange(undefined, '1', true),
          val: new SimpleChange(undefined, 'foo', true),
        },
      },
      {
        name: 'child 2',
        changes: {
          name: new SimpleChange(undefined, '2', true),
          val: new SimpleChange(undefined, 'foo', true),
        },
      },
    ]);

    events.length = 0;
    fixture.componentInstance.val = 'bar';
    fixture.detectChanges();

    expect(events).toEqual([
      {
        name: 'parent 1',
        changes: {
          val: new SimpleChange('foo', 'bar', false),
        },
      },
      {
        name: 'parent 2',
        changes: {
          val: new SimpleChange('foo', 'bar', false),
        },
      },
      {
        name: 'child 1',
        changes: {
          val: new SimpleChange('foo', 'bar', false),
        },
      },
      {
        name: 'child 2',
        changes: {
          val: new SimpleChange('foo', 'bar', false),
        },
      },
    ]);
  });


it('should call onChanges properly in for loop with children', () => {
    const events: any[] = [];

    @Component({
      selector: 'child',
      template: `<p>{{val}}</p>`,
      standalone: false,
    })
    class Child {
      @Input() val = '';

      @Input() name = '';

      ngOnChanges(changes: SimpleChanges) {
        events.push({name: 'child of parent ' + this.name, changes});
      }
    }

    @Component({
      selector: 'parent',
      template: `<child [name]="name" [val]="val"></child>`,
      standalone: false,
    })
    class Parent {
      @Input() val = '';

      @Input() name = '';

      ngOnChanges(changes: SimpleChanges) {
        events.push({name: 'parent ' + this.name, changes});
      }
    }

    @Component({
      template: `
        <parent name="0" [val]="val"></parent>
        <parent *ngFor="let number of numbers" [name]="number" [val]="val"></parent>
        <parent name="1" [val]="val"></parent>
      `,
      standalone: false,
    })
    class App {
      val = 'a';
      numbers = ['2', '3', '4'];
    }

    TestBed.configureTestingModule({
      declarations: [App, Child, Parent],
      imports: [CommonModule],
    });
    const fixture = TestBed.createComponent(App);
    fixture.detectChanges();

    expect(events).toEqual([
      {
        name: 'parent 0',
        changes: {
          name: new SimpleChange(undefined, '0', true),
          val: new SimpleChange(undefined, 'a', true),
        },
      },
      {
        name: 'parent 1',
        changes: {
          name: new SimpleChange(undefined, '1', true),
          val: new SimpleChange(undefined, 'a', true),
        },
      },
      {
        name: 'parent 2',
        changes: {
          name: new SimpleChange(undefined, '2', true),
          val: new SimpleChange(undefined, 'a', true),
        },
      },
      {
        name: 'child of parent 2',
        changes: {
          name: new SimpleChange(undefined, '2', true),
          val: new SimpleChange(undefined, 'a', true),
        },
      },
      {
        name: 'parent 3',
        changes: {
          name: new SimpleChange(undefined, '3', true),
          val: new SimpleChange(undefined, 'a', true),
        },
      },
      {
        name: 'child of parent 3',
        changes: {
          name: new SimpleChange(undefined, '3', true),
          val: new SimpleChange(undefined, 'a', true),
        },
      },
      {
        name: 'parent 4',
        changes: {
          name: new SimpleChange(undefined, '4', true),
          val: new SimpleChange(undefined, 'a', true),
        },
      },
      {
        name: 'child of parent 4',
        changes: {
          name: new SimpleChange(undefined, '4', true),
          val: new SimpleChange(undefined, 'a', true),
        },
      },
      {
        name: 'child of parent 0',
        changes: {
          name: new SimpleChange(undefined, '0', true),
          val: new SimpleChange(undefined, 'a', true),
        },
      },
      {
        name: 'child of parent 1',
        changes: {
          name: new SimpleChange(undefined, '1', true),
          val: new SimpleChange(undefined, 'a', true),
        },
      },
    ]);

    events.length = 0;
    fixture.componentInstance.val = 'b';
    fixture.detectChanges();

    expect(events).toEqual([
      {
        name: 'parent 0',
        changes: {
          val: new SimpleChange('a', 'b', false),
        },
      },
      {
        name: 'parent 1',
        changes: {
          val: new SimpleChange('a', 'b', false),
        },
      },
      {
        name: 'parent 2',
        changes: {
          val: new SimpleChange('a', 'b', false),
        },
      },
      {
        name: 'child of parent 2',
        changes: {
          val: new SimpleChange('a', 'b', false),
        },
      },
      {
        name: 'parent 3',
        changes: {
          val: new SimpleChange('a', 'b', false),
        },
      },
      {
        name: 'child of parent 3',
        changes: {
          val: new SimpleChange('a', 'b', false),
        },
      },
      {
        name: 'parent 4',
        changes: {
          val: new SimpleChange('a', 'b', false),
        },
      },
      {
        name: 'child of parent 4',
        changes: {
          val: new SimpleChange('a', 'b', false),
        },
      },
      {
        name: 'child of parent 0',
        changes: {
          val: new SimpleChange('a', 'b', false),
        },
      },
      {
        name: 'child of parent 1',
        changes: {
          val: new SimpleChange('a', 'b', false),
        },
      },
    ]);
  });

  it('should not call onChanges if props are set directly', () => {
    const events: any[] = [];

    @Component({
      template: `<p>{{value}}</p>`,
      standalone: false,
    })
    class App {
      value = 'a';
      ngOnChanges(changes: SimpleChanges) {
        events.push(changes);
      }
    }

    TestBed.configureTestingModule({
      declarations: [App],
    });
    const fixture = TestBed.createComponent(App);
    fixture.detectChanges();

    expect(events).toEqual([]);

    fixture.componentInstance.value = 'b';
    fixture.detectChanges();

    expect(events).toEqual([]);
  });
});


scribe('OnPush', () => {
    @Component({
      selector: 'my-comp',
      changeDetection: ChangeDetectionStrategy.OnPush,
      template: `{{ doCheckCount }} - {{ name }} <button (click)="onClick()"></button>`,
      standalone: false,
    })
    class MyComponent implements DoCheck {
      @Input() name = 'Nancy';
      doCheckCount = 0;

      ngDoCheck(): void {
        this.doCheckCount++;
      }

      onClick() {}
    }

    @Component({
      selector: 'my-app',
      template: '<my-comp [name]="name"></my-comp>',
      standalone: false,
    })
    class MyApp {
      @ViewChild(MyComponent) comp!: MyComponent;
      name: string = 'Nancy';
    }

    it('should check OnPush components on initialization', () => {
      TestBed.configureTestingModule({declarations: [MyComponent, MyApp]});
      const fixture = TestBed.createComponent(MyApp);
      fixture.detectChanges();

      expect(fixture.nativeElement.textContent.trim()).toEqual('1 - Nancy');
    });

    it('should call doCheck even when OnPush components are not dirty', () => {
      TestBed.configureTestingModule({declarations: [MyComponent, MyApp]});
      const fixture = TestBed.createComponent(MyApp);
      fixture.detectChanges();

      fixture.detectChanges();
      expect(fixture.componentInstance.comp.doCheckCount).toEqual(2);

      fixture.detectChanges();
      expect(fixture.componentInstance.comp.doCheckCount).toEqual(3);
    });

    it('should skip OnPush components in update mode when they are not dirty', () => {
      TestBed.configureTestingModule({declarations: [MyComponent, MyApp]});
      const fixture = TestBed.createComponent(MyApp);
      fixture.detectChanges();

      // doCheckCount is 2, but 1 should be rendered since it has not been marked dirty.
      expect(fixture.nativeElement.textContent.trim()).toEqual('1 - Nancy');

      fixture.detectChanges();

      // doCheckCount is 3, but 1 should be rendered since it has not been marked dirty.
      expect(fixture.nativeElement.textContent.trim()).toEqual('1 - Nancy');
    });

    it('should check OnPush components in update mode when inputs change', () => {
      TestBed.configureTestingModule({declarations: [MyComponent, MyApp]});
      const fixture = TestBed.createComponent(MyApp);
      fixture.detectChanges();

      fixture.componentInstance.name = 'Bess';
      fixture.detectChanges();

      expect(fixture.componentInstance.comp.doCheckCount).toEqual(2);
      // View should update, as changed input marks view dirty
      expect(fixture.nativeElement.textContent.trim()).toEqual('2 - Bess');

      fixture.componentInstance.name = 'George';
      fixture.detectChanges();

      // View should update, as changed input marks view dirty
      expect(fixture.componentInstance.comp.doCheckCount).toEqual(3);
      expect(fixture.nativeElement.textContent.trim()).toEqual('3 - George');

      fixture.detectChanges();

      expect(fixture.componentInstance.comp.doCheckCount).toEqual(4);
      // View should not be updated to "4", as inputs have not changed.
      expect(fixture.nativeElement.textContent.trim()).toEqual('3 - George');
    });

    it('should check OnPush components in update mode when component events occur', () => {
      TestBed.configureTestingModule({declarations: [MyComponent, MyApp]});
      const fixture = TestBed.createComponent(MyApp);
      fixture.detectChanges();

      expect(fixture.componentInstance.comp.doCheckCount).toEqual(1);
      expect(fixture.nativeElement.textContent.trim()).toEqual('1 - Nancy');

      const button = fixture.nativeElement.querySelector('button')!;
      button.click();

      // No ticks should have been scheduled.
      expect(fixture.componentInstance.comp.doCheckCount).toEqual(1);
      expect(fixture.nativeElement.textContent.trim()).toEqual('1 - Nancy');

      fixture.detectChanges();

      // Because the onPush comp should be dirty, it should update once CD runs
      expect(fixture.componentInstance.comp.doCheckCount).toEqual(2);
      expect(fixture.nativeElement.textContent.trim()).toEqual('2 - Nancy');
    });

    it('should not check OnPush components in update mode when parent events occur', () => {
      @Component({
        selector: 'button-parent',
        template: '<my-comp></my-comp><button id="parent" (click)="noop()"></button>',
        standalone: false,
      })
      class ButtonParent {
        @ViewChild(MyComponent) comp!: MyComponent;
        noop() {}
      }

      TestBed.configureTestingModule({declarations: [MyComponent, ButtonParent]});
      const fixture = TestBed.createComponent(ButtonParent);
      fixture.detectChanges();

      expect(fixture.nativeElement.textContent.trim()).toEqual('1 - Nancy');

      const button: HTMLButtonElement = fixture.nativeElement.querySelector('button#parent');
      button.click();
      fixture.detectChanges();

      // The comp should still be clean. So doCheck will run, but the view should display 1.
      expect(fixture.componentInstance.comp.doCheckCount).toEqual(2);
      expect(fixture.nativeElement.textContent.trim()).toEqual('1 - Nancy');
    });

    it('should check parent OnPush components in update mode when child events occur', () => {
      @Component({
        selector: 'button-parent',
        template: '{{ doCheckCount }} - <my-comp></my-comp>',
        changeDetection: ChangeDetectionStrategy.OnPush,
        standalone: false,
      })
      class ButtonParent implements DoCheck {
        @ViewChild(MyComponent) comp!: MyComponent;
        noop() {}

        doCheckCount = 0;
        ngDoCheck(): void {
          this.doCheckCount++;
        }
      }

      @Component({
        selector: 'my-button-app',
        template: '<button-parent></button-parent>',
        standalone: false,
      })
      class MyButtonApp {
        @ViewChild(ButtonParent) parent!: ButtonParent;
      }

      TestBed.configureTestingModule({declarations: [MyButtonApp, MyComponent, ButtonParent]});
      const fixture = TestBed.createComponent(MyButtonApp);
      fixture.detectChanges();

      const parent = fixture.componentInstance.parent;
      const comp = parent.comp;

      expect(parent.doCheckCount).toEqual(1);
      expect(comp.doCheckCount).toEqual(1);
      expect(fixture.nativeElement.textContent.trim()).toEqual('1 - 1 - Nancy');

      fixture.detectChanges();
      expect(parent.doCheckCount).toEqual(2);
      // parent isn't checked, so child doCheck won't run
      expect(comp.doCheckCount).toEqual(1);
      expect(fixture.nativeElement.textContent.trim()).toEqual('1 - 1 - Nancy');

      const button = fixture.nativeElement.querySelector('button');
      button.click();

      // No ticks should have been scheduled.
      expect(parent.doCheckCount).toEqual(2);
      expect(comp.doCheckCount).toEqual(1);

      fixture.detectChanges();
      expect(parent.doCheckCount).toEqual(3);
      expect(comp.doCheckCount).toEqual(2);
      expect(fixture.nativeElement.textContent.trim()).toEqual('3 - 2 - Nancy');
    });

    it('should check parent OnPush components when child directive on a template emits event', fakeAsync(() => {
      @Directive({
        selector: '[emitter]',
        standalone: false,
      })
      class Emitter {
        @Output() event = new EventEmitter<string>();

        ngOnInit() {
          setTimeout(() => {
            this.event.emit('new message');
          });
        }
      }

      @Component({
        selector: 'my-app',
        template: '{{message}} <ng-template emitter (event)="message = $event"></ng-template>',
        changeDetection: ChangeDetectionStrategy.OnPush,
        standalone: false,
      })
      class MyApp {
        message = 'initial message';
      }

      const fixture = TestBed.configureTestingModule({
        declarations: [MyApp, Emitter],
      }).createComponent(MyApp);
      fixture.detectChanges();

      expect(fixture.nativeElement.textContent.trim()).toEqual('initial message');
      tick();
      fixture.detectChanges();
      expect(fixture.nativeElement.textContent.trim()).toEqual('new message');
    }));
  });


scribe('ChangeDetectorRef', () => {
    describe('detectChanges()', () => {
      @Component({
        selector: 'my-comp',
        template: '{{ name }}',
        changeDetection: ChangeDetectionStrategy.OnPush,
        standalone: false,
      })
      class MyComp implements DoCheck {
        doCheckCount = 0;
        name = 'Nancy';

        constructor(public cdr: ChangeDetectorRef) {}

        ngDoCheck() {
          this.doCheckCount++;
        }
      }

      @Component({
        selector: 'parent-comp',
        template: `{{ doCheckCount}} - <my-comp></my-comp>`,
        standalone: false,
      })
      class ParentComp implements DoCheck {
        @ViewChild(MyComp) myComp!: MyComp;

        doCheckCount = 0;

        constructor(public cdr: ChangeDetectorRef) {}

        ngDoCheck() {
          this.doCheckCount++;
        }
      }

      @Directive({
        selector: '[dir]',
        standalone: false,
      })
      class Dir {
        constructor(public cdr: ChangeDetectorRef) {}
      }

      it('should check the component view when called by component (even when OnPush && clean)', () => {
        TestBed.configureTestingModule({declarations: [MyComp]});
        const fixture = TestBed.createComponent(MyComp);
        fixture.detectChanges();

        expect(fixture.nativeElement.textContent).toEqual('Nancy');

        fixture.componentInstance.name = 'Bess'; // as this is not an Input, the component stays clean
        fixture.componentInstance.cdr.detectChanges();
        expect(fixture.nativeElement.textContent).toEqual('Bess');
      });

      it('should NOT call component doCheck when called by a component', () => {
        TestBed.configureTestingModule({declarations: [MyComp]});
        const fixture = TestBed.createComponent(MyComp);
        fixture.detectChanges();

        expect(fixture.componentInstance.doCheckCount).toEqual(1);

        // NOTE: in current Angular, detectChanges does not itself trigger doCheck, but you
        // may see doCheck called in some cases bc of the extra CD run triggered by zone.js.
        // It's important not to call doCheck to allow calls to detectChanges in that hook.
        fixture.componentInstance.cdr.detectChanges();
        expect(fixture.componentInstance.doCheckCount).toEqual(1);
      });

      it('should NOT check the component parent when called by a child component', () => {
        TestBed.configureTestingModule({declarations: [MyComp, ParentComp]});
        const fixture = TestBed.createComponent(ParentComp);
        fixture.detectChanges();

        expect(fixture.nativeElement.textContent).toEqual('1 - Nancy');

        fixture.componentInstance.doCheckCount = 100;
        fixture.componentInstance.myComp.cdr.detectChanges();
        expect(fixture.componentInstance.doCheckCount).toEqual(100);
        expect(fixture.nativeElement.textContent).toEqual('1 - Nancy');
      });

      it('should check component children when called by component if dirty or check-always', () => {
        TestBed.configureTestingModule({declarations: [MyComp, ParentComp]});
        const fixture = TestBed.createComponent(ParentComp);
        fixture.detectChanges();
        expect(fixture.componentInstance.doCheckCount).toEqual(1);

        fixture.componentInstance.myComp.name = 'Bess';
        fixture.componentInstance.cdr.detectChanges();
        expect(fixture.componentInstance.doCheckCount).toEqual(1);
        expect(fixture.componentInstance.myComp.doCheckCount).toEqual(2);
        // OnPush child is not dirty, so its change isn't rendered.
        expect(fixture.nativeElement.textContent).toEqual('1 - Nancy');
      });

      it('should not group detectChanges calls (call every time)', () => {
        TestBed.configureTestingModule({declarations: [MyComp, ParentComp]});
        const fixture = TestBed.createComponent(ParentComp);
        fixture.detectChanges();

        expect(fixture.componentInstance.doCheckCount).toEqual(1);

        fixture.componentInstance.cdr.detectChanges();
        fixture.componentInstance.cdr.detectChanges();
        expect(fixture.componentInstance.myComp.doCheckCount).toEqual(3);
      });

      it('should check component view when called by directive on component node', () => {
        @Component({
          template: '<my-comp dir></my-comp>',
          standalone: false,
        })
        class MyApp {
          @ViewChild(MyComp) myComp!: MyComp;
          @ViewChild(Dir) dir!: Dir;
        }

        TestBed.configureTestingModule({declarations: [MyComp, Dir, MyApp]});
        const fixture = TestBed.createComponent(MyApp);
        fixture.detectChanges();

        expect(fixture.nativeElement.textContent).toEqual('Nancy');

        fixture.componentInstance.myComp.name = 'George';
        fixture.componentInstance.dir.cdr.detectChanges();
        expect(fixture.nativeElement.textContent).toEqual('George');
      });

      it('should check host component when called by directive on element node', () => {
        @Component({
          template: '{{ value }}

',
          standalone: false,
        })
        class MyApp {
          @ViewChild(MyComp) myComp!: MyComp;
          @ViewChild(Dir) dir!: Dir;
          value = '';
        }

        TestBed.configureTestingModule({declarations: [Dir, MyApp]});
        const fixture = TestBed.createComponent(MyApp);
        fixture.detectChanges();

        fixture.componentInstance.value = 'Frank';
        fixture.detectChanges();
        expect(fixture.nativeElement.textContent).toEqual('Frank');

        fixture.componentInstance.value = 'Joe';
        fixture.componentInstance.dir.cdr.detectChanges();
        expect(fixture.nativeElement.textContent).toEqual('Joe');
      });

      it('should check the host component when called from EmbeddedViewRef', () => {
        @Component({
          template: '{{ name }}

',
          standalone: false,
        })
        class MyApp {
          @ViewChild(Dir) dir!: Dir;
          showing = true;
          name = 'Amelia';
        }

        TestBed.configureTestingModule({declarations: [Dir, MyApp], imports: [CommonModule]});
        const fixture = TestBed.createComponent(MyApp);
        fixture.detectChanges();

        expect(fixture.nativeElement.textContent).toEqual('Amelia');

        fixture.componentInstance.name = 'Emerson';
        fixture.componentInstance.dir.cdr.detectChanges();
        expect(fixture.nativeElement.textContent).toEqual('Emerson');
      });

      it('should support call in ngOnInit', () => {
        @Component({
          template: '{{ value }}',
          standalone: false,
        })
        class DetectChangesComp implements OnInit {
          value = 0;

          constructor(public cdr: ChangeDetectorRef) {}

          ngOnInit() {
            this.value++;
            this.cdr.detectChanges();
          }
        }

        TestBed.configureTestingModule({declarations: [DetectChangesComp]});
        const fixture = TestBed.createComponent(DetectChangesComp);
        fixture.detectChanges();

        expect(fixture.nativeElement.textContent).toEqual('1');
      });

      ['OnInit', 'AfterContentInit', 'AfterViewInit', 'OnChanges'].forEach((hook) => {
        it(`should not go infinite loop when recursively called from children's ng${hook}`, () => {
          @Component({
            template: '<child-comp [inp]="true"></child-comp>',
            standalone: false,
          })
          class ParentComp {
            constructor(public cdr: ChangeDetectorRef) {}
            triggerChangeDetection() {
              this.cdr.detectChanges();
            }
          }

          @Component({
            template: '{{inp}}',
            selector: 'child-comp',
            standalone: false,
          })
          class ChildComp {
            @Input() inp: any = '';

            count = 0;
            constructor(public parentComp: ParentComp) {}

            ngOnInit() {
              this.check('OnInit');
            }
            ngAfterContentInit() {
              this.check('AfterContentInit');
            }
            ngAfterViewInit() {
              this.check('AfterViewInit');
            }
            ngOnChanges() {
              this.check('OnChanges');
            }

            check(h: string) {
              if (h === hook) {
                this.count++;
                if (this.count > 1) throw new Error(`ng${hook} should be called only once!`);
                this.parentComp.triggerChangeDetection();
              }
            }
          }

          TestBed.configureTestingModule({declarations: [ParentComp, ChildComp]});

          expect(() => {
            const fixture = TestBed.createComponent(ParentComp);
            fixture.detectChanges();
          }).not.toThrow();
        });
      });

      it('should support call in ngDoCheck', () => {
        @Component({
          template: '{{doCheckCount}}',
          standalone: false,
        })
        class DetectChangesComp {
          doCheckCount = 0;

          constructor(public cdr: ChangeDetectorRef) {}

          ngDoCheck() {
            this.doCheckCount++;
            this.cdr.detectChanges();
          }
        }

        TestBed.configureTestingModule({declarations: [DetectChangesComp]});
        const fixture = TestBed.createComponent(DetectChangesComp);
        fixture.detectChanges();

        expect(fixture.nativeElement.textContent).toEqual('1');
      });


('should support change detection triggered as a result of View queries processing', () => {
        @Component({
          selector: 'app',
          template: `
            <div *ngIf="visible" #ref>Visible text</div>
          `,
          standalone: false,
        })
        class App {
          @ViewChildren('ref') ref!: QueryList<any>;

          visible = false;

          constructor(public changeDetectorRef: ChangeDetectorRef) {}

          ngAfterViewInit() {
            this.ref.changes.subscribe((refs: QueryList<any>) => {
              this.visible = false;
              this.changeDetectorRef.detectChanges();
            });
          }
        }

        TestBed.configureTestingModule({
          declarations: [App],
          imports: [CommonModule],
        });
        const fixture = TestBed.createComponent(App);
        fixture.detectChanges();
        expect(fixture.nativeElement.textContent).toBe('');

        // even though we set "visible" to `true`, we do not expect any content to be displayed,
        // since the flag is overridden in `ngAfterViewInit` back to `false`
        fixture.componentInstance.visible = true;
        fixture.detectChanges();
        expect(fixture.nativeElement.textContent).toBe('');
      });

      describe('dynamic views', () => {
        @Component({
          selector: 'structural-comp',
          template: '{{ value }}',
          standalone: false,
        })
        class StructuralComp {
          @Input() tmp!: TemplateRef<any>;
          value = 'one';

          constructor(public vcr: ViewContainerRef) {}

          create() {
            return this.vcr.createEmbeddedView(this.tmp, {ctx: this});
          }
        }

        it('should support ViewRef.detectChanges()', () => {
          @Component({
            template:
              '<ng-template #foo let-ctx="ctx">{{ ctx.value }}</ng-template><structural-comp [tmp]="foo"></structural-comp>',
            standalone: false,
          })
          class App {
            @ViewChild(StructuralComp) structuralComp!: StructuralComp;
          }

          TestBed.configureTestingModule({declarations: [App, StructuralComp]});
          const fixture = TestBed.createComponent(App);
          fixture.detectChanges();

          expect(fixture.nativeElement.textContent).toEqual('one');

          const viewRef: EmbeddedViewRef<any> = fixture.componentInstance.structuralComp.create();
          fixture.detectChanges();
          expect(fixture.nativeElement.textContent).toEqual('oneone');

          // check embedded view update
          fixture.componentInstance.structuralComp.value = 'two';
          viewRef.detectChanges();
          expect(fixture.nativeElement.textContent).toEqual('onetwo');

          // check root view update
          fixture.componentInstance.structuralComp.value = 'three';
          fixture.detectChanges();
          expect(fixture.nativeElement.textContent).toEqual('threethree');
        });

        it('should support ViewRef.detectChanges() directly after creation', () => {
          @Component({
            template: '<ng-template #foo>Template text</ng-template><structural-comp [tmp]="foo">',
            standalone: false,
          })
          class App {
            @ViewChild(StructuralComp) structuralComp!: StructuralComp;
          }

          TestBed.configureTestingModule({declarations: [App, StructuralComp]});
          const fixture = TestBed.createComponent(App);
          fixture.detectChanges();

          expect(fixture.nativeElement.textContent).toEqual('one');

          const viewRef: EmbeddedViewRef<any> = fixture.componentInstance.structuralComp.create();
          viewRef.detectChanges();
          expect(fixture.nativeElement.textContent).toEqual('oneTemplate text');
        });
      });
    });

    describe('attach/detach', () => {
      @Component({
        selector: 'detached-comp',
        template: '{{ value }}',
        standalone: false,
      })
      class DetachedComp implements DoCheck {
        value = 'one';
        doCheckCount = 0;

        constructor(public cdr: ChangeDetectorRef) {}

        ngDoCheck() {
          this.doCheckCount++;
        }
      }

      @Component({
        template: '<detached-comp></detached-comp>',
        standalone: false,
      })
      class MyApp {
        @ViewChild(DetachedComp) comp!: DetachedComp;

        constructor(public cdr: ChangeDetectorRef) {}
      }

      it('should not check detached components', () => {
        TestBed.configureTestingModule({declarations: [MyApp, DetachedComp]});
        const fixture = TestBed.createComponent(MyApp);
        fixture.detectChanges();

        expect(fixture.nativeElement.textContent).toEqual('one');

        fixture.componentInstance.comp.cdr.detach();

        fixture.componentInstance.comp.value = 'two';
        fixture.detectChanges();
        expect(fixture.nativeElement.textContent).toEqual('one');
      });

      it('should check re-attached components', () => {
        TestBed.configureTestingModule({declarations: [MyApp, DetachedComp]});
        const fixture = TestBed.createComponent(MyApp);
        fixture.detectChanges();

        expect(fixture.nativeElement.textContent).toEqual('one');

        fixture.componentInstance.comp.cdr.detach();
        fixture.componentInstance.comp.value = 'two';

        fixture.componentInstance.comp.cdr.reattach();
        fixture.detectChanges();
        expect(fixture.nativeElement.textContent).toEqual('two');
      });

      it('should call lifecycle hooks on detached components', () => {
        TestBed.configureTestingModule({declarations: [MyApp, DetachedComp]});
        const fixture = TestBed.createComponent(MyApp);
        fixture.detectChanges();

        expect(fixture.componentInstance.comp.doCheckCount).toEqual(1);

        fixture.componentInstance.comp.cdr.detach();

        fixture.detectChanges();
        expect(fixture.componentInstance.comp.doCheckCount).toEqual(2);
      });

      it('should check detached component when detectChanges is called', () => {
        TestBed.configureTestingModule({declarations: [MyApp, DetachedComp]});
        const fixture = TestBed.createComponent(MyApp);
        fixture.detectChanges();

        expect(fixture.nativeElement.textContent).toEqual('one');

        fixture.componentInstance.comp.cdr.detach();

        fixture.componentInstance.comp.value = 'two';
        fixture.componentInstance.comp.cdr.detectChanges();

        expect(fixture.nativeElement.textContent).toEqual('two');
      });

      it('should not check detached component when markDirty is called', () => {
        TestBed.configureTestingModule({declarations: [MyApp, DetachedComp]});
        const fixture = TestBed.createComponent(MyApp);
        fixture.detectChanges();
        const comp = fixture.componentInstance.comp;

        comp.cdr.detach();
        comp.value = 'two';
        comp.cdr.markForCheck();
        fixture.detectChanges();

        expect(fixture.nativeElement.textContent).toEqual('one');
      });

      it('should detach any child components when parent is detached', () => {
        TestBed.configureTestingModule({declarations: [MyApp, DetachedComp]});
        const fixture = TestBed.createComponent(MyApp);
        fixture.detectChanges();

        expect(fixture.nativeElement.textContent).toEqual('one');

        fixture.componentInstance.cdr.detach();

        fixture.componentInstance.comp.value = 'two';
        fixture.detectChanges();
        expect(fixture.nativeElement.textContent).toEqual('one');

        fixture.componentInstance.cdr.reattach();

        fixture.detectChanges();
        expect(fixture.nativeElement.textContent).toEqual('two');
      });

      it('should detach OnPush components properly', () => {
        @Component({
          selector: 'on-push-comp',
          template: '{{ value }}',
          changeDetection: ChangeDetectionStrategy.OnPush,
          standalone: false,
        })
        class OnPushComp {
          @Input() value!: string;

          constructor(public cdr: ChangeDetectorRef) {}
        }

        @Component({
          template: '<on-push-comp [value]="value"></on-push-comp>',
          standalone: false,
        })
        class OnPushApp {
          @ViewChild(OnPushComp) onPushComp!: OnPushComp;
          value = '';
        }

        TestBed.configureTestingModule({declarations: [OnPushApp, OnPushComp]});
        const fixture = TestBed.createComponent(OnPushApp);
        fixture.detectChanges();

        fixture.componentInstance.value = 'one';
        fixture.detectChanges();
        expect(fixture.nativeElement.textContent).toEqual('one');

        fixture.componentInstance.onPushComp.cdr.detach();

        fixture.componentInstance.value = 'two';
        fixture.detectChanges();
        expect(fixture.nativeElement.textContent).toEqual('one');

        fixture.componentInstance.onPushComp.cdr.reattach();

        fixture.detectChanges();
        expect(fixture.nativeElement.textContent).toEqual('two');
      });
    });