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

import {computed, signal} from '@angular/core';
import {createWatch} from '@angular/core/primitives/signals';

import {flushEffects, resetEffects, testingEffect} from './effect_util';

const NOOP_FN = () => {};

describe('watchers', () => {
  afterEach(() => {
    resetEffects();
  });

  it('should create and run once, even without dependencies', () => {
    let runs = 0;

    testingEffect(() => {
      runs++;
    });

    flushEffects();
    expect(runs).toEqual(1);
  });

  it('should schedule on dependencies (signal) change', () => {
    const count = signal(0);
    let runLog: number[] = [];
    const effectRef = testingEffect(() => {
      runLog.push(count());
    });

    flushEffects();
    expect(runLog).toEqual([0]);

    count.set(1);
    flushEffects();
    expect(runLog).toEqual([0, 1]);
  });

  it('should not schedule when a previous dependency changes', () => {
    const increment = (value: number) => value + 1;
    const countA = signal(0);
    const countB = signal(100);
    const useCountA = signal(true);

    const runLog: number[] = [];
    testingEffect(() => {
      runLog.push(useCountA() ? countA() : countB());
    });

    flushEffects();
    expect(runLog).toEqual([0]);

    countB.update(increment);
    flushEffects();
    // No update expected: updated the wrong signal.
    expect(runLog).toEqual([0]);

    countA.update(increment);
    flushEffects();
    expect(runLog).toEqual([0, 1]);

    useCountA.set(false);
    flushEffects();
    expect(runLog).toEqual([0, 1, 101]);

    countA.update(increment);
    flushEffects();
    // No update expected: updated the wrong signal.
    expect(runLog).toEqual([0, 1, 101]);
  });

  it("should not update dependencies when dependencies don't change", () => {
    const source = signal(0);
    const isEven = computed(() => source() % 2 === 0);
    let updateCounter = 0;
    testingEffect(() => {
      isEven();
      updateCounter++;
    });

    flushEffects();
    expect(updateCounter).toEqual(1);

    source.set(1);
    flushEffects();
    expect(updateCounter).toEqual(2);

    source.set(3);
    flushEffects();
    expect(updateCounter).toEqual(2);

    source.set(4);
    flushEffects();
    expect(updateCounter).toEqual(3);
  });

  it('should allow registering cleanup function from the watch logic', () => {
    const source = signal(0);

    const seenCounterValues: number[] = [];
    testingEffect((onCleanup) => {
      seenCounterValues.push(source());

      // register a cleanup function that is executed every time an effect re-runs
      onCleanup(() => {
        if (seenCounterValues.length === 2) {
          seenCounterValues.length = 0;
        }
      });
    });

    flushEffects();
    expect(seenCounterValues).toEqual([0]);

    source.update((c) => c + 1);
    flushEffects();
    expect(seenCounterValues).toEqual([0, 1]);

    source.update((c) => c + 1);
    flushEffects();
    // cleanup (array trim) should have run before executing effect
    expect(seenCounterValues).toEqual([2]);
  });

  it('should forget previously registered cleanup function when effect re-runs', () => {
    const source = signal(0);

    const seenCounterValues: number[] = [];
    testingEffect((onCleanup) => {
      const value = source();

      seenCounterValues.push(value);

      // register a cleanup function that is executed next time an effect re-runs
      if (value === 0) {
        onCleanup(() => {
          seenCounterValues.length = 0;
        });
      }
    });

    flushEffects();
    expect(seenCounterValues).toEqual([0]);

    source.set(2);
    flushEffects();
    // cleanup (array trim) should have run before executing effect
    expect(seenCounterValues).toEqual([2]);

    source.set(3);
    flushEffects();
    // cleanup (array trim) should _not_ be registered again
    expect(seenCounterValues).toEqual([2, 3]);
  });

  it('should throw an error when reading a signal during the notification phase', () => {
    const source = signal(0);
    let ranScheduler = false;
    const w = createWatch(
      () => {
        source();
      },
      () => {
        ranScheduler = true;
        expect(() => source()).toThrow();
      },
      false,
    );

    // Run the effect manually to initiate dependency tracking.
    w.run();

    // Changing the signal will attempt to schedule the effect.
    source.set(1);
    expect(ranScheduler).toBeTrue();
  });

  describe('destroy', () => {
    it('should not run destroyed watchers', () => {
      let watchRuns = 0;
      const watchRef = createWatch(
        () => {
          watchRuns++;
        },
        NOOP_FN,
        false,
      );

      watchRef.run();
      expect(watchRuns).toBe(1);

      watchRef.destroy();
      watchRef.run();
      expect(watchRuns).toBe(1);
    });

    it('should disconnect destroyed watches from the reactive graph', () => {
      const counter = signal(0);

      let scheduleCount = 0;
      const watchRef = createWatch(
        () => counter(),
        () => scheduleCount++,
        false,
      );

      // watches are _not_ scheduled by default, run it for the first time to capture
      // dependencies
      watchRef.run();
      expect(scheduleCount).toBe(0);

      watchRef.destroy();
      counter.set(1);
      expect(scheduleCount).toBe(0);
    });

    it('should not schedule destroyed watches', () => {
      let scheduleCount = 0;
      const watchRef = createWatch(NOOP_FN, () => scheduleCount++, false);

      // watches are _not_ scheduled by default
      expect(scheduleCount).toBe(0);

      watchRef.notify();
      expect(scheduleCount).toBe(1);

      watchRef.destroy();
      watchRef.notify();
      expect(scheduleCount).toBe(1);
    });

    it('should not run cleanup functions after destroy', () => {
      const counter = signal(0);
      let cleanupRuns = 0;
      const watchRef = createWatch(
        (onCleanup) => {
          counter();
          onCleanup(() => cleanupRuns++);
        },
        NOOP_FN,
        false,
      );

      // initial run to register cleanup function
      watchRef.run();

      watchRef.destroy();
      // cleanup functions run on destroy
      expect(cleanupRuns).toBe(1);

      // subsequent destroy should be noop
      watchRef.destroy();
      expect(cleanupRuns).toBe(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 {computed, signal, untracked} from '@angular/core';

import {flushEffects, resetEffects, testingEffect} from './effect_util';

describe('non-reactive reads', () => {
  afterEach(() => {
    resetEffects();
  });

  it('should read the latest value from signal', () => {
    const counter = signal(0);

    expect(untracked(counter)).toEqual(0);

    counter.set(1);
    expect(untracked(counter)).toEqual(1);
  });

  it('should not add dependencies to computed when reading a value from a signal', () => {
    const counter = signal(0);
    const double = computed(() => untracked(counter) * 2);

    expect(double()).toEqual(0);

    counter.set(2);
    expect(double()).toEqual(0);
  });

  it('should refresh computed value if stale and read non-reactively ', () => {
    const counter = signal(0);
    const double = computed(() => counter() * 2);

    expect(untracked(double)).toEqual(0);

    counter.set(2);
    expect(untracked(double)).toEqual(4);
  });

  it('should not make surrounding effect depend on the signal', () => {
    const s = signal(1);

    const runLog: number[] = [];
    testingEffect(() => {
      runLog.push(untracked(s));
    });

    // an effect will run at least once
    flushEffects();
    expect(runLog).toEqual([1]);

    // subsequent signal changes should not trigger effects as signal is untracked
    s.set(2);
    flushEffects();
    expect(runLog).toEqual([1]);
  });

  it('should schedule on dependencies (computed) change', () => {
    const count = signal(0);
    const double = computed(() => count() * 2);

    let runLog: number[] = [];
    testingEffect(() => {
      runLog.push(double());
    });

    flushEffects();
    expect(runLog).toEqual([0]);

    count.set(1);
    flushEffects();
    expect(runLog).toEqual([0, 2]);
  });

  it('should non-reactively read all signals accessed inside untrack', () => {
    const first = signal('John');
    const last = signal('Doe');

    let runLog: string[] = [];
    const effectRef = testingEffect(() => {
      untracked(() => runLog.push(`${first()} ${last()}`));
    });

    // effects run at least once
    flushEffects();
    expect(runLog).toEqual(['John Doe']);

    // change one of the signals - should not update as not read reactively
    first.set('Patricia');
    flushEffects();
    expect(runLog).toEqual(['John Doe']);

    // change one of the signals - should not update as not read reactively
    last.set('Garcia');
    flushEffects();
    expect(runLog).toEqual(['John Doe']);
  });
});


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

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

describe('computed', () => {
  it('should create computed', () => {
    const counter = signal(0);

    let computedRunCount = 0;
    const double = computed(() => `${counter() * 2}:${++computedRunCount}`);

    expect(double()).toEqual('0:1');

    counter.set(1);
    expect(double()).toEqual('2:2');
    expect(double()).toEqual('2:2');

    counter.set(2);
    expect(double()).toEqual('4:3');
    expect(double()).toEqual('4:3');
  });

  it('should not re-compute if there are no dependencies', () => {
    let tick = 0;
    const c = computed(() => ++tick);

    expect(c()).toEqual(1);
    expect(c()).toEqual(1);
  });

  it('should not re-compute if the dependency is a primitive value and the value did not change', () => {
    const counter = signal(0);

    let computedRunCount = 0;
    const double = computed(() => `${counter() * 2}:${++computedRunCount}`);

    expect(double()).toEqual('0:1');

    counter.set(0);
    expect(double()).toEqual('0:1');
  });

  it('should chain computed', () => {
    const name = signal('abc');
    const reverse = computed(() => name().split('').reverse().join(''));
    const upper = computed(() => reverse().toUpperCase());

    expect(upper()).toEqual('CBA');

    name.set('foo');
    expect(upper()).toEqual('OOF');
  });

  it('should evaluate computed only when subscribing', () => {
    const name = signal('John');
    const age = signal(25);
    const show = signal(true);

    let computeCount = 0;
    const displayName = computed(
      () => `${show() ? `${name()} aged ${age()}` : 'anonymous'}:${++computeCount}`,
    );

    expect(displayName()).toEqual('John aged 25:1');

    show.set(false);
    expect(displayName()).toEqual('anonymous:2');

    name.set('Bob');
    expect(displayName()).toEqual('anonymous:2');
  });

  it('should detect simple dependency cycles', () => {
    const a: () => unknown = computed(() => a());
    expect(() => a()).toThrowError('Detected cycle in computations.');
  });

  it('should detect deep dependency cycles', () => {
    const a: () => unknown = computed(() => b());
    const b = computed(() => c());
    const c = computed(() => d());
    const d = computed(() => a());
    expect(() => a()).toThrowError('Detected cycle in computations.');
  });

  it('should cache exceptions thrown until computed gets dirty again', () => {
    const toggle = signal('KO');
    const c = computed(() => {
      const val = toggle();
      if (val === 'KO') {
        throw new Error('KO');
      } else {
        return val;
      }
    });

    expect(() => c()).toThrowError('KO');
    expect(() => c()).toThrowError('KO');

    toggle.set('OK');
    expect(c()).toEqual('OK');
  });

  it("should not update dependencies of computations when dependencies don't change", () => {
    const source = signal(0);
    const isEven = computed(() => source() % 2 === 0);
    let updateCounter = 0;
    const updateTracker = computed(() => {
      isEven();
      return updateCounter++;
    });

    updateTracker();
    expect(updateCounter).toEqual(1);

    source.set(1);
    updateTracker();
    expect(updateCounter).toEqual(2);

    // Setting the counter to another odd value should not trigger `updateTracker` to update.
    source.set(3);
    updateTracker();
    expect(updateCounter).toEqual(2);

    source.set(4);
    updateTracker();
    expect(updateCounter).toEqual(3);
  });

  it('should not mark dirty computed signals that are dirty already', () => {
    const source = signal('a');
    const derived = computed(() => source().toUpperCase());

    let watchCount = 0;
    const w = createWatch(
      () => {
        derived();
      },
      () => {
        watchCount++;
      },
      false,
    );

    w.run();
    expect(watchCount).toEqual(0);

    // change signal, mark downstream dependencies dirty
    source.set('b');
    expect(watchCount).toEqual(1);

    // change signal again, downstream dependencies should be dirty already and not marked again
    source.set('c');
    expect(watchCount).toEqual(1);

    // resetting dependencies back to clean
    w.run();
    expect(watchCount).toEqual(1);

    // expecting another notification at this point
    source.set('d');
    expect(watchCount).toEqual(2);
  });

  it('should allow signal creation within computed', () => {
    const doubleCounter = computed(() => {
      const counter = signal(1);
      return counter() * 2;
    });

    expect(doubleCounter()).toBe(2);
  });

  it('should disallow writing to signals within computed', () => {
    const source = signal(0);
    const illegal = computed(() => {
      source.set(1);
      return 0;
    });

    expect(illegal).toThrow();
  });

  it('should have a toString implementation', () => {
    const counter = signal(1);
    const double = computed(() => counter() * 2);
    expect(double + '').toBe('[Computed: 2]');
  });

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


/**
 * @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.io/license
 */

import {isSignal, linkedSignal, signal, computed} from '@angular/core';
import {testingEffect} from './effect_util';

describe('linkedSignal', () => {
  it('should conform to the writable signals contract', () => {
    const firstLetter = linkedSignal({source: signal<string>('foo'), computation: (str) => str[0]});
    expect(isSignal(firstLetter)).toBeTrue();

    firstLetter.set('a');
    expect(firstLetter()).toBe('a');

    firstLetter.update((_) => 'b');
    expect(firstLetter()).toBe('b');

    const firstLetterReadonly = firstLetter.asReadonly();
    expect(firstLetterReadonly()).toBe('b');
    firstLetter.set('c');
    expect(firstLetterReadonly()).toBe('c');

    expect(firstLetter.toString()).toBe('[LinkedSignal: c]');
  });

  it('should conform to the writable signals contract - shorthand', () => {
    const str = signal<string>('foo');
    const firstLetter = linkedSignal(() => str()[0]);
    expect(isSignal(firstLetter)).toBeTrue();

    firstLetter.set('a');
    expect(firstLetter()).toBe('a');

    firstLetter.update((_) => 'b');
    expect(firstLetter()).toBe('b');

    const firstLetterReadonly = firstLetter.asReadonly();
    expect(firstLetterReadonly()).toBe('b');
    firstLetter.set('c');
    expect(firstLetterReadonly()).toBe('c');
  });

  it('should update when the source changes', () => {
    const options = signal(['apple', 'banana', 'fig']);
    const choice = linkedSignal({
      source: options,
      computation: (options) => options[0],
    });

    expect(choice()).toBe('apple');

    choice.set('fig');
    expect(choice()).toBe('fig');

    options.set(['orange', 'apple', 'pomegranate']);
    expect(choice()).toBe('orange');
  });

  it('should expose previous source in the computation function', () => {
    const options = signal(['apple', 'banana', 'fig']);
    const choice = linkedSignal<string[], string>({
      source: options,
      computation: (options, previous) => {
        if (previous !== undefined && options.includes(previous.value)) {
          return previous.value;
        } else {
          return options[0];
        }
      },
    });

    expect(choice()).toBe('apple');

    options.set(['orange', 'apple', 'pomegranate']);
    expect(choice()).toBe('apple');
  });

  it('should expose previous value in the computation function', () => {
    const options = signal(['apple', 'banana', 'fig']);
    const choice = linkedSignal<string[], number>({
      source: options,
      computation: (options, previous) => {
        if (previous !== undefined) {
          const prevChoice = previous.source[previous.value];
          const newIdx = options.indexOf(prevChoice);
          return newIdx === -1 ? 0 : newIdx;
        } else {
          return 0;
        }
      },
    });

    expect(choice()).toBe(0);

    choice.set(2); // choosing a fig
    options.set(['banana', 'fig', 'apple']); // a fig moves to a new place in the updated resource
    expect(choice()).toBe(1);
  });

  it('should expose previous value in the computation function in larger reactive graph', () => {
    const options = signal(['apple', 'banana', 'fig']);
    const choice = linkedSignal<string[], number>({
      source: options,
      computation: (options, previous) => {
        if (previous !== undefined) {
          const prevChoice = previous.source[previous.value];
          const newIdx = options.indexOf(prevChoice);
          return newIdx === -1 ? 0 : newIdx;
        } else {
          return 0;
        }
      },
    });

    const doubleChoice = computed(() => choice() * 2);

    expect(doubleChoice()).toBe(0);

    choice.set(2); // choosing a fig
    options.set(['banana', 'fig', 'apple']); // a fig moves to a new place in the updated resource

    expect(doubleChoice()).toBe(2);
    expect(choice()).toBe(1);
  });

  it('should not update the value if the new choice is considered equal to the previous one', () => {
    const counter = signal(0);
    const choice = linkedSignal({
      source: counter,
      computation: (c) => c,
      equal: (a, b) => true,
    });

    expect(choice()).toBe(0);

    // updates from the "commanding signal" should follow equality rules
    counter.update((c) => c + 1);
    expect(choice()).toBe(0);

    // the same equality rules should apply to the state signal
    choice.set(10);
    expect(choice()).toBe(0);
  });

  it('should support shorthand version', () => {
    const options = signal(['apple', 'banana', 'fig']);
    const choice = linkedSignal(() => options()[0]);

    expect(choice()).toBe('apple');

    choice.set('orange');
    expect(options()).toEqual(['apple', 'banana', 'fig']);
    expect(choice()).toBe('orange');

    options.set(['banana', 'fig']);
    expect(choice()).toBe('banana');
  });

  it('should have explicitly set value', () => {
    const counter = signal(0);
    const options = signal(['apple', 'banana', 'fig']);
    const choice = linkedSignal(() => options()[0]);
    expect(choice()).toBe('apple');

    // state signal is updated while the "commanding" state has pending changes - we assume that the explicit state set is "more important" here
    options.set(['orange', 'apple', 'pomegranate']);
    choice.set('watermelon');
    // unrelated state changes should not effect the test results
    counter.update((c) => c + 1);
    expect(choice()).toBe('watermelon');

    // state signal is updated just before changes to the "commanding" state - here we default to the usual situation of the linked state triggering computation
    choice.set('persimmon');
    options.set(['apple', 'banana', 'fig']);
    expect(choice()).toBe('apple');

    // another change using the "update" code-path
    options.set(['orange', 'apple', 'pomegranate']);
    choice.update((f) => f.toUpperCase());
    expect(choice()).toBe('ORANGE');
  });

  it('should have explicitly set value - live consumer', () => {
    const options = signal(['apple', 'banana', 'fig']);
    const choice = linkedSignal(() => options()[0]);
    expect(choice()).toBe('apple');
    // create an effect to mark the linkedSignal as live consumer
    const watchDestroy = testingEffect(() => choice());

    try {
      // state signal is updated while the "commanding" state has pending changes - we assume that the explicit state set is "more important" here
      const counter = signal(0);
      options.set(['orange', 'apple', 'pomegranate']);
      choice.set('watermelon');
      // unrelated state changes should not effect the test results
      counter.update((c) => c + 1);
      expect(choice()).toBe('watermelon');

      // state signal is updated just before changes to the "commanding" state - here we default to the usual situation of the linked state triggering computation
      choice.set('persimmon');
      options.set(['apple', 'banana', 'fig']);
      expect(choice()).toBe('apple');

      // another change using the "update" code-path
      options.set(['orange', 'apple', 'pomegranate']);
      choice.update((f) => f.toUpperCase());
      expect(choice()).toBe('ORANGE');
    } finally {
      watchDestroy();
    }
  });

  it('should capture linked signal dependencies even if the value is set before the initial read', () => {
    const options = signal(['apple', 'banana', 'fig']);
    const choice = linkedSignal(() => options()[0]);

    choice.set('watermelon');
    expect(choice()).toBe('watermelon');

    options.set(['orange', 'apple', 'pomegranate']);
    expect(choice()).toBe('orange');
  });

  it('should pull latest dependency values before state update', () => {
    const options = signal(['apple', 'banana', 'fig']);
    const choice = linkedSignal(() => options()[0]);

    choice.update((fruit) => fruit.toUpperCase());
    expect(choice()).toBe('APPLE');
  });

  it('should reset error state after manual state update', () => {
    const choice = linkedSignal<string>(() => {
      throw new Error("Can't compute");
    });

    expect(() => {
      choice();
    }).toThrowError("Can't compute");

    choice.set('explicit');
    expect(choice()).toBe('explicit');
  });
});


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

import {computed, isSignal, signal} from '@angular/core';

describe('isSignal', () => {
  it('should return true for writable signal', () => {
    const writableSignal = signal('Angular');
    expect(isSignal(writableSignal)).toBe(true);
  });

  it('should return true for readonly signal', () => {
    const readonlySignal = computed(() => 10);
    expect(isSignal(readonlySignal)).toBe(true);
  });

  it('should return false for primitive', () => {
    const primitive = 0;
    expect(isSignal(primitive)).toBe(false);
  });

  it('should return false for object', () => {
    const object = {name: 'Angular'};
    expect(isSignal(object)).toBe(false);
  });

  it('should return false for function', () => {
    const fn = () => {};
    expect(isSignal(fn)).toBe(false);
  });
});


nction () {
  // these tests are only meant to be run within the DOM (for now)
  if (isNode) return;

  describe('animation tests', function () {
    function getLog(): MockAnimationPlayer[] {
      return MockAnimationDriver.log as MockAnimationPlayer[];
    }

    function resetLog() {
      MockAnimationDriver.log = [];
    }

    beforeEach(() => {
      resetLog();
      TestBed.configureTestingModule({
        providers: [{provide: AnimationDriver, useClass: MockAnimationDriver}],
        imports: [BrowserAnimationsModule],
      });
    });

    describe('animation modules', function () {
      it('should hint at BrowserAnimationsModule being used', () => {
        TestBed.resetTestingModule();
        TestBed.configureTestingModule({
          declarations: [SharedAnimationCmp],
          imports: [BrowserAnimationsModule],
        });

        const fixture = TestBed.createComponent(SharedAnimationCmp);
        expect(fixture.componentInstance.animationType).toEqual('BrowserAnimations');
      });

      it('should hint at NoopAnimationsModule being used', () => {
        TestBed.resetTestingModule();
        TestBed.configureTestingModule({
          declarations: [SharedAnimationCmp],
          imports: [NoopAnimationsModule],
        });

        const fixture = TestBed.createComponent(SharedAnimationCmp);
        expect(fixture.componentInstance.animationType).toEqual('NoopAnimations');
      });

      it('should hint at NoopAnimationsModule being used when BrowserAnimationsModule is provided with disabled animations', () => {
        TestBed.resetTestingModule();
        TestBed.configureTestingModule({
          declarations: [SharedAnimationCmp],
          imports: [BrowserAnimationsModule.withConfig({disableAnimations: true})],
        });

        const fixture = TestBed.createComponent(SharedAnimationCmp);
        expect(fixture.componentInstance.animationType).toEqual('NoopAnimations');
      });
    });

    @Component({
      template: '
template text
',
      standalone: false,
    })
    class SharedAnimationCmp {
      constructor(
        @Inject(ANIMATION_MODULE_TYPE) public animationType: 'NoopAnimations' | 'BrowserAnimations',
      ) {}
    }

    describe('fakeAsync testing', () => {
      it('should only require one flushMicrotasks call to kick off animation callbacks', fakeAsync(() => {
        @Component({
          selector: 'cmp',
          template: `

          `,
          animations: [
            trigger('myAnimation', [
              transition('* => on, * => off', [animate(1000, style({opacity: 1}))]),
            ]),
          ],
          standalone: false,
        })
        class Cmp {
          exp: any = false;
          status: string = '';
          cb(status: string) {
            this.status = status;
          }
        }

        TestBed.configureTestingModule({declarations: [Cmp]});
        const fixture = TestBed.createComponent(Cmp);
        const cmp = fixture.componentInstance;
        cmp.exp = 'on';
        fixture.detectChanges();
        expect(cmp.status).toEqual('');

        flushMicrotasks();
        expect(cmp.status).toEqual('start');

        let player = MockAnimationDriver.log.pop()!;
        player.finish();
        expect(cmp.status).toEqual('done');

        cmp.status = '';
        cmp.exp = 'off';
        fixture.detectChanges();
        expect(cmp.status).toEqual('');

        player = MockAnimationDriver.log.pop()!;
        player.finish();
        expect(cmp.status).toEqual('');
        flushMicrotasks();
        expect(cmp.status).toEqual('done');
      }));

      it('should always run .start callbacks before .done callbacks even for noop animations', fakeAsync(() => {
        @Component({
          selector: 'cmp',
          template: `

          `,
          animations: [trigger('myAnimation', [transition('* => go', [])])],
          standalone: false,
        })
        class Cmp {
          exp: any = false;
          log: string[] = [];
          cb(status: string) {
            this.log.push(status);
          }
        }

        TestBed.configureTestingModule({declarations: [Cmp]});
        const fixture = TestBed.createComponent(Cmp);
        const cmp = fixture.componentInstance;
        cmp.exp = 'go';
        fixture.detectChanges();
        expect(cmp.log).toEqual([]);

        flushMicrotasks();
        expect(cmp.log).toEqual(['start', 'done']);
      }));

      it('should emit the correct totalTime value for a noop-animation', fakeAsync(() => {
        @Component({
          selector: 'cmp',
          template: `

          `,
          animations: [
            trigger('myAnimation', [transition('* => go', [animate('1s', style({opacity: 0}))])]),
          ],
          standalone: false,
        })
        class Cmp {
          exp: any = false;
          log: AnimationEvent[] = [];
          cb(event: AnimationEvent) {
            this.log.push(event);
          }
        }

        TestBed.configureTestingModule({
          declarations: [Cmp],
          providers: [{provide: AnimationDriver, useClass: NoopAnimationDriver}],
        });

        const fixture = TestBed.createComponent(Cmp);
        const cmp = fixture.componentInstance;
        cmp.exp = 'go';
        fixture.detectChanges();
        expect(cmp.log).toEqual([]);

        flushMicrotasks();
        expect(cmp.log.length).toEqual(2);
        const [start, end] = cmp.log;
        expect(start.totalTime).toEqual(1000);
        expect(end.totalTime).toEqual(1000);
      }));
    });

    describe('component fixture integration', () => {
      describe('whenRenderingDone', () => {
        it('should wait until the animations are finished until continuing', fakeAsync(() => {
          @Component({
            selector: 'cmp',
            template: `

            `,
            animations: [
              trigger('myAnimation', [transition('* => on', [animate(1000, style({opacity: 1}))])]),
            ],
            standalone: false,
          })
          class Cmp {
            exp: any = false;
          }

          TestBed.configureTestingModule({declarations: [Cmp]});
          const engine = TestBed.inject(ɵAnimationEngine);
          const fixture = TestBed.createComponent(Cmp);
          const cmp = fixture.componentInstance;

          let isDone = false;
          fixture.whenRenderingDone().then(() => (isDone = true));
          expect(isDone).toBe(false);

          cmp.exp = 'on';
          fixture.detectChanges();
          engine.flush();
          expect(isDone).toBe(false);

          const players = engine.players;
          expect(players.length).toEqual(1);
          players[0].finish();
          expect(isDone).toBe(false);

          flushMicrotasks();
          expect(isDone).toBe(true);
        }));

        it('should wait for a noop animation to finish before continuing', fakeAsync(() => {
          @Component({
            selector: 'cmp',
            template: `

            `,
            animations: [
              trigger('myAnimation', [transition('* => on', [animate(1000, style({opacity: 1}))])]),
            ],
            standalone: false,
          })
          class Cmp {
            exp: any = false;
          }

          TestBed.configureTestingModule({
            providers: [{provide: AnimationDriver, useClass: NoopAnimationDriver}],
            declarations: [Cmp],
          });

          const engine = TestBed.inject(ɵAnimationEngine);
          const fixture = TestBed.createComponent(Cmp);
          const cmp = fixture.componentInstance;

          let isDone = false;
          fixture.whenRenderingDone().then(() => (isDone = true));
          expect(isDone).toBe(false);

          cmp.exp = 'off';
          fixture.detectChanges();
          engine.flush();
          expect(isDone).toBe(false);

          flushMicrotasks();
          expect(isDone).toBe(true);
        }));

        it('should wait for active animations to finish even if they have already started', fakeAsync(() => {
          @Component({
            selector: 'cmp',
            template: `

              `,
            animations: [
              trigger('myAnimation', [transition('* => on', [animate(1000, style({opacity: 1}))])]),
            ],
            standalone: false,
          })
          class Cmp {
            exp: any = false;
          }

          TestBed.configureTestingModule({declarations: [Cmp]});
          const engine = TestBed.inject(ɵAnimationEngine);
          const fixture = TestBed.createComponent(Cmp);
          const cmp = fixture.componentInstance;
          cmp.exp = 'on';
          fixture.detectChanges();
          engine.flush();

          const players = engine.players;
          expect(players.length).toEqual(1);

          let isDone = false;
          fixture.whenRenderingDone().then(() => (isDone = true));
          flushMicrotasks();
          expect(isDone).toBe(false);

          players[0].finish();
          flushMicrotasks();
          expect(isDone).toBe(true);
        }));
      });
    });


ransitions', () => {
      @Component({
        selector: 'if-cmp',
        template: `

        `,
        animations: [
          trigger('myAnimation', [
            transition('void => *', [style({opacity: 0}), animate('0.5s 1s', style({opacity: 1}))]),
            transition('* => void', [
              animate(1000, style({height: 0})),
              animate(1000, style({opacity: 0})),
            ]),
          ]),
        ],
        standalone: false,
      })
      class Cmp {
        exp: any = false;
      }

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

      expect(() => {
        TestBed.createComponent(Cmp);
      }).not.toThrowError();
    });

    it("should add the transition provided delay to all the transition's timelines", () => {
      @Component({
        selector: 'cmp',
        template: `

     `,
        animations: [
          trigger('parent', [
            transition(
              ':enter',
              [
                style({background: 'red'}),
                group(
                  [
                    animate('1s 3s ease', style({background: 'green'})),
                    query('@child', animateChild()),
                  ],
                  {delay: 111},
                ),
              ],
              {delay: '2s'},
            ),
          ]),
          trigger('child', [
            transition(
              ':enter',
              [style({color: 'white'}), animate('2s 3s ease', style({color: 'black'}))],
              {delay: 222},
            ),
          ]),
        ],
        standalone: false,
      })
      class Cmp {
        exp: boolean = false;
      }

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

      const engine = TestBed.inject(ɵAnimationEngine);
      const fixture = TestBed.createComponent(Cmp);
      const cmp = fixture.componentInstance;
      cmp.exp = true;

      fixture.detectChanges();
      engine.flush();

      const players = getLog();
      expect(players.length).toEqual(4);
      // players:
      //  - scp (skipped child player): player for the child animation
      //  - pp1 (parent player 1): player for parent animation (from background red to red)
      //  - pp2 (parent player 2): player for parent animation (from background red to green)
      //  - pcp (parent child player):
      //     player for child animation executed by parent via query and animateChild
      const [scp, pp1, pp2, pcp] = players;
      expect(scp.delay).toEqual(222);
      expect(pp1.delay).toEqual(2000);
      expect(pp2.delay).toEqual(2111); // 2000 + 111
      expect(pcp.delay).toEqual(0); // all the delays are included in the child animation
      expect(pcp.duration).toEqual(7333); // 2s + 3s + 2000 + 111 + 222
    });

    it('should keep (transition from/to) styles defined in different timelines', () => {
      @Component({
        selector: 'cmp',
        template: '

',
        animations: [
          trigger('animation', [
            transition(':enter', [
              group([
                style({opacity: 0, color: 'red'}),
                // Note: the objective of this test is to make sure the animation
                // transitions from opacity 0 and color red to opacity 1 and color blue,
                // even though the two styles are defined in different timelines
                animate(500, style({opacity: 1, color: 'blue'})),
              ]),
            ]),
          ]),
        ],
        standalone: false,
      })
      class Cmp {
        exp: boolean = false;
      }

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

      const engine = TestBed.inject(ɵAnimationEngine);
      const fixture = TestBed.createComponent(Cmp);
      const cmp = fixture.componentInstance;
      cmp.exp = true;

      fixture.detectChanges();
      engine.flush();

      const players = getLog();
      expect(players.length).toEqual(1);

      const [player] = players;

      expect(player.keyframes).toEqual([
        new Map<string, string | number>([
          ['opacity', '0'],
          ['color', 'red'],
          ['offset', 0],
        ]),
        new Map<string, string | number>([
          ['opacity', '1'],
          ['color', 'blue'],
          ['offset', 1],
        ]),
      ]);
    });

    describe('errors for not using the animation module', () => {
      beforeEach(() => {
        TestBed.configureTestingModule({
          providers: [{provide: RendererFactory2, useExisting: ɵDomRendererFactory2}],
        });
      });

      function syntheticPropError(name: string, nameKind: string) {
        return `NG05105: Unexpected synthetic ${nameKind} ${name} found. Please make sure that:
  - Either \`BrowserAnimationsModule\` or \`NoopAnimationsModule\` are imported in your application.
  - There is corresponding configuration for the animation named \`${name}\` defined in the \`animations\` field of the \`@Component\` decorator (see https://angular.io/api/core/Component#animations).`;
      }

      describe('when modules are missing', () => {
        it('should throw when using an @prop binding without the animation module', () => {
          @Component({
            template: `<div [@myAnimation]="true"></div>`,
            standalone: false,
          })
          class Cmp {}

          TestBed.configureTestingModule({declarations: [Cmp]});
          const comp = TestBed.createComponent(Cmp);
          expect(() => comp.detectChanges()).toThrowError(
            syntheticPropError('@myAnimation', 'property'),
          );
        });

        it('should throw when using an @prop listener without the animation module', () => {
          @Component({
            template: `<div (@myAnimation.start)="a = true"></div>`,
            standalone: false,
          })
          class Cmp {
            a = false;
          }

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

          expect(() => TestBed.createComponent(Cmp)).toThrowError(
            syntheticPropError('@myAnimation.start', 'listener'),
          );
        });
      });

      describe('when modules are present, but animations are missing', () => {
        it('should throw when using an @prop property, BrowserAnimationModule is imported, but there is no animation rule', () => {
          @Component({
            template: `<div [@myAnimation]="true"></div>`,
            standalone: false,
          })
          class Cmp {}

          TestBed.configureTestingModule({declarations: [Cmp], imports: [BrowserAnimationsModule]});
          const comp = TestBed.createComponent(Cmp);
          expect(() => comp.detectChanges()).toThrowError(
            syntheticPropError('@myAnimation', 'property'),
          );
        });

        it('should throw when using an @prop listener, BrowserAnimationModule is imported, but there is no animation rule', () => {
          @Component({
            template: `<div (@myAnimation.start)="true"></div>`,
            standalone: false,
          })
          class Cmp {}

          TestBed.configureTestingModule({declarations: [Cmp], imports: [BrowserAnimationsModule]});

          expect(() => TestBed.createComponent(Cmp)).toThrowError(
            syntheticPropError('@myAnimation.start', 'listener'),
          );
        });
      });
    });

    describe('non-animatable css props', () => {
      functio


/**
 * @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 {
  animate,
  animateChild,
  AnimationPlayer,
  AUTO_STYLE,
  group,
  query,
  sequence,
  stagger,
  state,
  style,
  transition,
  trigger,
  ɵAnimationGroupPlayer as AnimationGroupPlayer,
} from '@angular/animations';
import {
  AnimationDriver,
  ɵAnimationEngine,
  ɵnormalizeKeyframes as normalizeKeyframes,
} from '@angular/animations/browser';
import {TransitionAnimationPlayer} from '@angular/animations/browser/src/render/transition_animation_engine';
import {ENTER_CLASSNAME, LEAVE_CLASSNAME} from '@angular/animations/browser/src/util';
import {MockAnimationDriver, MockAnimationPlayer} from '@angular/animations/browser/testing';
import {CommonModule} from '@angular/common';
import {Component, HostBinding, ViewChild} from '@angular/core';
import {fakeAsync, flushMicrotasks, TestBed} from '@angular/core/testing';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';

import {HostListener} from '../../src/metadata/directives';

(function () {


/**
 * @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 {
  animate,
  animateChild,
  group,
  query,
  sequence,
  style,
  transition,
  trigger,
  ɵAnimationGroupPlayer as AnimationGroupPlayer,
} from '@angular/animations';
import {AnimationDriver, ɵAnimationEngine} from '@angular/animations/browser';
import {TransitionAnimationPlayer} from '@angular/animations/browser/src/render/transition_animation_engine';
import {MockAnimationDriver, MockAnimationPlayer} from '@angular/animations/browser/testing';
import {Component, HostBinding} from '@angular/core';
import {fakeAsync, flushMicrotasks, TestBed, tick} from '@angular/core/testing';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {ActivatedRoute, Router, RouterModule, RouterOutlet} from '@angular/router';

(function () {
  // these tests are only meant to be run within the DOM (for now)
  if (isNode) return;

  describe('Animation Router Tests', function () {
    function getLog(): MockAnimationPlayer[] {
      return MockAnimationDriver.log as MockAnimationPlayer[];
    }

    function resetLog() {
      MockAnimationDriver.log = [];
    }

    beforeEach(() => {
      resetLog();
      TestBed.configureTestingModule({
        imports: [RouterModule.forRoot([]), BrowserAnimationsModule],
        providers: [{provide: AnimationDriver, useClass: MockAnimationDriver}],
      });
    });

    it('should query the old and new routes via :leave and :enter', fakeAsync(() => {
      @Component({
        animations: [
          trigger('routerAnimations', [
            transition('page1 => page2', [
              query(':leave', animateChild()),
              query(':enter', animateChild()),
            ]),
          ]),
        ],
        template: `

            <router-outlet #r="outlet"></router-outlet>

        `,
        standalone: false,
      })
      class ContainerCmp {
        constructor(public router: Router) {}

        prepareRouteAnimation(r: RouterOutlet) {
          const animation = r.activatedRouteData['animation'];
          const value = animation ? animation['value'] : null;
          return value;
        }
      }

      @Component({
        selector: 'page1',
        template: `page1`,
        animations: [
          trigger('page1Animation', [
            transition(':leave', [style({width: '200px'}), animate(1000, style({width: '0px'}))]),
          ]),
        ],
        standalone: false,
      })
      class Page1Cmp {
        @HostBinding('@page1Animation') public doAnimate = true;
      }

      @Component({
        selector: 'page2',
        template: `page2`,
        animations: [
          trigger('page2Animation', [
            transition(':enter', [style({opacity: 0}), animate(1000, style({opacity: 1}))]),
          ]),
        ],
        standalone: false,
      })
      class Page2Cmp {
        @HostBinding('@page2Animation') public doAnimate = true;
      }

      TestBed.configureTestingModule({
        declarations: [Page1Cmp, Page2Cmp, ContainerCmp],
        imports: [
          RouterModule.forRoot([
            {path: 'page1', component: Page1Cmp, data: makeAnimationData('page1')},
            {path: 'page2', component: Page2Cmp, data: makeAnimationData('page2')},
          ]),
        ],
      });

      const engine = TestBed.inject(ɵAnimationEngine);
      const fixture = TestBed.createComponent(ContainerCmp);
      const cmp = fixture.componentInstance;
      cmp.router.initialNavigation();
      tick();
      fixture.detectChanges();
      engine.flush();

      cmp.router.navigateByUrl('/page1');
      tick();
      fixture.detectChanges();
      engine.flush();

      cmp.router.navigateByUrl('/page2');
      tick();
      fixture.detectChanges();
      engine.flush();

      const player = engine.players[0]!;
      const groupPlayer = (
        player as TransitionAnimationPlayer
      ).getRealPlayer() as AnimationGroupPlayer;
      const players = groupPlayer.players as MockAnimationPlayer[];

      expect(players.length).toEqual(2);
      const [p1, p2] = players;

      expect(p1.duration).toEqual(1000);
      expect(p1.keyframes).toEqual([
        new Map<string, string | number>([
          ['offset', 0],
          ['width', '200px'],
        ]),
        new Map<string, string | number>([
          ['offset', 1],
          ['width', '0px'],
        ]),
      ]);

      expect(p2.duration).toEqual(2000);
      expect(p2.keyframes).toEqual([
        new Map<string, string | number>([
          ['offset', 0],
          ['opacity', '0'],
        ]),
        new Map<string, string | number>([
          ['offset', 0.5],
          ['opacity', '0'],
        ]),
        new Map<string, string | number>([
          ['offset', 1],
          ['opacity', '1'],
        ]),
      ]);
    }));

    it('should allow inner enter animations to be emulated within a routed item', fakeAsync(() => {
      @Component({
        animations: [
          trigger('routerAnimations', [
            transition('page1 => page2', [query(':enter', animateChild())]),
          ]),
        ],
        template: `

            <router-outlet #r="outlet"></router-outlet>

        `,
        standalone: false,
      })
      class ContainerCmp {
        constructor(public router: Router) {}

        prepareRouteAnimation(r: RouterOutlet) {
          const animation = r.activatedRouteData['animation'];
          const value = animation ? animation['value'] : null;
          return value;
        }
      }

      @Component({
        selector: 'page1',
        template: `page1`,
        animations: [],
        standalone: false,
      })
      class Page1Cmp {}

      @Component({
        selector: 'page2',
        template: `

Page 2

        `,
        animations: [
          trigger('page2Animation', [
            transition(':enter', [
              query('.if-one', animateChild()),
              query('.if-two', animateChild()),
            ]),
          ]),
          trigger('ifAnimation', [
            transition(':enter', [style({opacity: 0}), animate(1000, style({opacity: 1}))]),
          ]),
        ],
        standalone: false,
      })
      class Page2Cmp {
        @HostBinding('@page2Animation') public doAnimate = true;

        public exp = true;
      }

      TestBed.configureTestingModule({
        declarations: [Page1Cmp, Page2Cmp, ContainerCmp],
        imports: [
          RouterModule.forRoot([
            {path: 'page1', component: Page1Cmp, data: makeAnimationData('page1')},
            {path: 'page2', component: Page2Cmp, data: makeAnimationData('page2')},
          ]),
        ],
      });

      const engine = TestBed.inject(ɵAnimationEngine);
      const fixture = TestBed.createComponent(ContainerCmp);
      const cmp = fixture.componentInstance;
      cmp.router.initialNavigation();
      tick();
      fixture.detectChanges();
      engine.flush();

      cmp.router.navigateByUrl('/page1');
      tick();
      fixture.detectChanges();
      engine.flush();

      cmp.router.navigateByUrl('/page2');
      tick();
      fixture.detectChanges();
      engine.flush();

      const player = engine.players[0]!;
      const groupPlayer = (
        player as TransitionAnimationPlayer
      ).getRealPlayer() as AnimationGroupPlayer;
      const players = groupPlayer.players as MockAnimationPlayer[];

      expect(players.length).toEqual(2);
      const [p1, p2] = players;

      expect(p1.keyframes).toEqual([
        new Map<string, string | number>([
          ['offset', 0],
          ['opacity', '0'],
        ]),
        new Map<string, string | number>([
          ['offset', 1],
          ['opacity', '1'],
        ]),
      ]);

      expect(p2.keyframes).toEqual([
        new Map<string, string | number>([
          ['offset', 0],
          ['opacity', '0'],
        ]),
        new Map<string, string | number>([
          ['offset', 0.5],
          ['opacity', '0'],
        ]),
        new Map<string, string | number>([
          ['offset', 1],
          ['opacity', '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 {animate, query, state, style, transition, trigger} from '@angular/animations';
import {
  AnimationDriver,
  ɵAnimationEngine,
  ɵWebAnimationsDriver,
  ɵWebAnimationsPlayer,
} from '@angular/animations/browser';
import {TransitionAnimationPlayer} from '@angular/animations/browser/src/render/transition_animation_engine';
import {AnimationGroupPlayer} from '@angular/animations/src/players/animation_group_player';
import {Component, ViewChild} from '@angular/core';
import {TestBed} from '@angular/core/testing';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';

(


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

import {computed, isSignal, model, WritableSignal} from '@angular/core';
import {TestBed} from '@angular/core/testing';

describe('model signal', () => {
  it('should work with computed expressions', () => {
    const signal = TestBed.runInInjectionContext(() => model(0));
    let computedCount = 0;
    const derived = computed(() => (computedCount++, signal() + 1000));

    expect(derived()).toBe(1000);
    expect(computedCount).toBe(1);

    signal.set(1);
    expect(computedCount).toBe(1);

    expect(derived()).toBe(1001);
    expect(computedCount).toBe(2);
  });

  it('should allow updates based on the previous value', () => {
    const signal = TestBed.runInInjectionContext(() => model(2));

    signal.update((value) => value * 3);
    expect(signal()).toBe(6);

    signal.update((value) => value * 3);
    expect(signal()).toBe(18);
  });

  it('should return readonly signal', () => {
    const signal = TestBed.runInInjectionContext(() => model(2));
    const readOnly = signal.asReadonly();

    expect(isSignal(readOnly)).toBeTrue();
    expect(readOnly()).toBe(2);
    expect((readOnly as WritableSignal<unknown>).set).toBeUndefined();
    expect((readOnly as WritableSignal<unknown>).update).toBeUndefined();
  });

  it('should emit when the value changes', () => {
    const signal = TestBed.runInInjectionContext(() => model(1));
    const values: number[] = [];

    signal.subscribe((value) => values.push(value));

    signal.set(2);
    expect(values).toEqual([2]);

    signal.update((previous) => previous * 2);
    expect(values).toEqual([2, 4]);

    signal.set(5);
    expect(values).toEqual([2, 4, 5]);
  });

  it('should error when subscribing to a destroyed model', () => {
    const signal = TestBed.runInInjectionContext(() => model(1));
    const values: number[] = [];

    signal.subscribe((value) => values.push(value));

    TestBed.resetTestingModule();

    expect(() => signal.subscribe(() => {})).toThrowError(
      /Unexpected subscription to destroyed `OutputRef`/,
    );
  });

  it('should stop emitting after unsubscribing', () => {
    const signal = TestBed.runInInjectionContext(() => model(0));
    const values: number[] = [];
    const subscription = signal.subscribe((value) => values.push(value));

    signal.set(1);
    expect(values).toEqual([1]);

    subscription.unsubscribe();
    signal.set(2);
    expect(values).toEqual([1]);
  });

  it('should not emit if the value does not change', () => {
    const signal = TestBed.runInInjectionContext(() => model(0));
    const values: number[] = [];
    signal.subscribe((value) => values.push(value));

    signal.set(1);
    expect(values).toEqual([1]);
    signal.set(1);
    expect(values).toEqual([1]);

    signal.update((previous) => previous * 2);
    expect(values).toEqual([1, 2]);
    signal.update((previous) => previous * 1);
    expect(values).toEqual([1, 2]);
  });

  it('should not share subscriptions between models', () => {
    let emitCount = 0;
    const signal1 = TestBed.runInInjectionContext(() => model(0));
    const signal2 = TestBed.runInInjectionContext(() => model(0));
    const callback = () => emitCount++;
    const subscription1 = signal1.subscribe(callback);
    const subscription2 = signal2.subscribe(callback);

    signal1.set(1);
    expect(emitCount).toBe(1);

    signal2.set(1);
    expect(emitCount).toBe(2);

    subscription1.unsubscribe();
    signal2.set(2);
    expect(emitCount).toBe(3);

    subscription2.unsubscribe();
    signal2.set(3);
    expect(emitCount).toBe(3);
  });

  it('should throw if there is no value for required model', () => {
    const signal = TestBed.runInInjectionContext(() => model.required());

    expect(() => signal()).toThrowError(/Model is required but no value is available yet\./);

    signal.set(1);
    expect(signal()).toBe(1);
  });

  it('should throw if a `computed` depends on an uninitialized required model', () => {
    const signal = TestBed.runInInjectionContext(() => model.required<number>());
    const expr = computed(() => signal() + 1000);

    expect(() => expr()).toThrowError(/Model is required but no value is available yet\./);

    signal.set(1);
    expect(expr()).toBe(1001);
  });

  it('should have a toString implementation', () => {
    const signal = TestBed.runInInjectionContext(() => model(0));
    expect(signal + '').toBe('[Model Signal: 0]');
  });
});


report unknown property bindings on ng-content', () => {
      @Component({
        template: `<ng-content *unknownProp="123"></ng-content>`,
        standalone: false,
      })
      class App {}

      TestBed.configureTestingModule({declarations: [App]});
      const spy = spyOn(console, 'error');
      const fixture = TestBed.createComponent(App);
      fixture.detectChanges();

      expect(spy.calls.mostRecent()?.args[0]).toMatch(
        /Can't bind to 'unknownProp' since it isn't a known property of 'ng-content' \(used in the 'App' component template\)/,
      );
    });

    it('should throw an error on unknown property bindings on ng-content when errorOnUnknownProperties is enabled', () => {
      @Component({
        template: `<ng-content *unknownProp="123"></ng-content>`,
        standalone: false,
      })
      class App {}

      TestBed.configureTestingModule({declarations: [App], errorOnUnknownProperties: true});
      expect(() => {
        const fixture = TestBed.createComponent(App);
        fixture.detectChanges();
      }).toThrowError(
        /NG0303: Can't bind to 'unknownProp' since it isn't a known property of 'ng-content' \(used in the 'App' component template\)/g,
      );
    });

    it('should report unknown property bindings on ng-container', () => {
      @Component({
        template: `<ng-container [unknown-prop]="123"></ng-container>`,
        standalone: false,
      })
      class App {}

      TestBed.configureTestingModule({declarations: [App]});
      const spy = spyOn(console, 'error');
      const fixture = TestBed.createComponent(App);
      fixture.detectChanges();

      expect(spy.calls.mostRecent()?.args[0]).toMatch(
        /Can't bind to 'unknown-prop' since it isn't a known property of 'ng-container' \(used in the 'App' component template\)/,
      );
    });

    it('should throw error on unknown property bindings on ng-container when errorOnUnknownProperties is enabled', () => {
      @Component({
        template: `<ng-container [unknown-prop]="123"></ng-container>`,
        standalone: false,
      })
      class App {}

      TestBed.configureTestingModule({declarations: [App], errorOnUnknownProperties: true});
      expect(() => {
        const fixture = TestBed.createComponent(App);
        fixture.detectChanges();
      }).toThrowError(
        /NG0303: Can't bind to 'unknown-prop' since it isn't a known property of 'ng-container' \(used in the 'App' component template\)/g,
      );
    });

    it('should log an error on unknown props and include a note on Web Components', () => {
      @Component({
        selector: 'may-be-web-component',
        template: `...`,
        standalone: false,
      })
      class MaybeWebComp {}

      @Component({
        selector: 'my-comp',
        template: `<may-be-web-component [unknownProp]="condition"></may-be-web-component>`,
        standalone: false,
      })
      class MyComp {
        condition = true;
      }

      @NgModule({
        declarations: [MyComp, MaybeWebComp],
      })
      class MyModule {}

      TestBed.configureTestingModule({imports: [MyModule]});

      const spy = spyOn(console, 'error');
      const fixture = TestBed.createComponent(MyComp);
      fixture.detectChanges();

      const errorMessage = spy.calls.mostRecent().args[0];

      // Split the error message into chunks, so it's easier to debug if needed.
      const lines = [
        `NG0303: Can't bind to 'unknownProp' since it isn't a known property of 'may-be-web-component' \\(used in the 'MyComp' component template\\).`,
        `1. If 'may-be-web-component' is an Angular component and it has the 'unknownProp' input, then verify that it is a part of an @NgModule where this component is declared.`,
        `2. If 'may-be-web-component' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@NgModule.schemas' of this component to suppress this message.`,
        `3. To allow any property add 'NO_ERRORS_SCHEMA' to the '@NgModule.schemas' of this component.`,
      ];
      lines.forEach((line) => expect(errorMessage).toMatch(line));
    });

    KNOWN_CONTROL_FLOW_DIRECTIVES.forEach((correspondingImport, directive) => {
      it(
        `should produce a warning when the '${directive}' directive ` +
          `is used in a template, but not imported in corresponding NgModule`,
        () => {
          @Component({
            template: `<div *${directive}="expr"></div>`,
            standalone: false,
          })
          class App {
            expr = true;
          }

          @NgModule({
            declarations: [App],
          })
          class Module {}

          TestBed.configureTestingModule({imports: [Module]});
          const spy = spyOn(console, 'error');
          const fixture = TestBed.createComponent(App);
          fixture.detectChanges();

          const errorMessage = spy.calls.mostRecent()?.args[0];

          // Split the error message into chunks, so it's easier to debug if needed.
          const lines = [
            `NG0303: Can't bind to '${directive}' since it isn't a known property of 'div' \\(used in the 'App' component template\\).`,
            `If the '${directive}' is an Angular control flow directive, please make sure ` +
              `that either the '${correspondingImport}' directive or the 'CommonModule' is a part of an @NgModule where this component is declared.`,
          ];
          lines.forEach((line) => expect(errorMessage).toMatch(line));
        },
      );

      it(
        `should produce a warning when the '${directive}' directive ` +
          `is used in a template, but not imported in a standalone component`,
        () => {
          @Component({
            standalone: true,
            template: `<div *${directive}="expr"></div>`,
          })
          class App {
            expr = true;
          }

          const spy = spyOn(console, 'error');
          const fixture = TestBed.createComponent(App);
          fixture.detectChanges();

          const errorMessage = spy.calls.mostRecent()?.args[0];

          // Split the error message into chunks, so it's easier to debug if needed.
          const lines = [
            `NG0303: Can't bind to '${directive}' since it isn't a known property of 'div' \\(used in the 'App' component template\\).`,
            `If the '${directive}' is an Angular control flow directive, please make sure ` +
              `that either the '${correspondingImport}' directive or the 'CommonModule' is included in the '@Component.imports' of this component.`,
          ];
          lines.forEach((line) => expect(errorMessage).toMatch(line));
        },
      );
    });

    descr


/**
 * @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 {
  ChangeDetectorRef,
  Component,
  Directive,
  EmbeddedViewRef,
  Injectable,
  Injector,
  Input,
  TemplateRef,
  ViewChild,
  ViewContainerRef,
  ViewRef,
} from '@angular/core';
import {TestBed} from '@angular/core/testing';
import {By} from '@angular/platform-browser';

describe('view insertion', () => {
  describe('of a simple template', () => {
    it('should insert into an empty container, at the front, in the middle, and at the end', () => {
      let _counter = 0;

      @Component({
        selector: 'increment-comp',
        template: `<span>created{{counter}}</span>`,
        standalone: false,
      })
      class IncrementComp {
        counter = _counter++;
      }

      @Component({
        template: `
              <ng-template #simple><increment-comp></increment-comp></ng-template>

            `,
        standalone: false,
      })
      class App {
        @ViewChild('container', {read: ViewContainerRef, static: true})
        container: ViewContainerRef = null!;

        @ViewChild('simple', {read: TemplateRef, static: true}) simple: TemplateRef<any> = null!;

        view0: EmbeddedViewRef<any> = null!;
        view1: EmbeddedViewRef<any> = null!;
        view2: EmbeddedViewRef<any> = null!;
        view3: EmbeddedViewRef<any> = null!;

        constructor(public changeDetector: ChangeDetectorRef) {}

        ngAfterViewInit() {
          // insert at the front
          this.view1 = this.container.createEmbeddedView(this.simple); // "created0"

          // insert at the front again
          this.view0 = this.container.createEmbeddedView(this.simple, {}, 0); // "created1"

          // insert at the end
          this.view3 = this.container.createEmbeddedView(this.simple); // "created2"

          // insert in the middle
          this.view2 = this.container.createEmbeddedView(this.simple, {}, 2); // "created3"

          // We need to run change detection here to avoid
          // ExpressionChangedAfterItHasBeenCheckedError because of the value updating in
          // increment-comp
          this.changeDetector.detectChanges();
        }
      }

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

      expect(app.container.indexOf(app.view0)).toBe(0);
      expect(app.container.indexOf(app.view1)).toBe(1);
      expect(app.container.indexOf(app.view2)).toBe(2);
      expect(app.container.indexOf(app.view3)).toBe(3);
      // The text in each component differs based on *when* it was created.
      expect(fixture.nativeElement.textContent).toBe('created1created0created3created2');
    });
  });

  describe('of an empty template', () => {
    it('should insert into an empty container, at the front, in the middle, and at the end', () => {
      @Component({
        template: `
              <ng-template #empty></ng-template>

            `,
        standalone: false,
      })
      class App {
        @ViewChild('container', {read: ViewContainerRef}) container: ViewContainerRef = null!;

        @ViewChild('empty', {read: TemplateRef}) empty: TemplateRef<any> = null!;

        view0: EmbeddedViewRef<any> = null!;
        view1: EmbeddedViewRef<any> = null!;
        view2: EmbeddedViewRef<any> = null!;
        view3: EmbeddedViewRef<any> = null!;

        ngAfterViewInit() {
          // insert at the front
          this.view1 = this.container.createEmbeddedView(this.empty);

          // insert at the front again
          this.view0 = this.container.createEmbeddedView(this.empty, {}, 0);

          // insert at the end
          this.view3 = this.container.createEmbeddedView(this.empty);

          // insert in the middle
          this.view2 = this.container.createEmbeddedView(this.empty, {}, 2);
        }
      }

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

      expect(app.container.indexOf(app.view0)).toBe(0);
      expect(app.container.indexOf(app.view1)).toBe(1);
      expect(app.container.indexOf(app.view2)).toBe(2);
      expect(app.container.indexOf(app.view3)).toBe(3);
    });
  });

  describe('of an ng-content projection', () => {
    it('should insert into an empty container, at the front, in the middle, and at the end', () => {
      @Component({
        selector: 'comp',
        template: `
                  <ng-template #projection><ng-content></ng-content></ng-template>

                `,
        standalone: false,
      })
      class Comp {
        @ViewChild('container', {read: ViewContainerRef}) container: ViewContainerRef = null!;

        @ViewChild('projection', {read: TemplateRef}) projection: TemplateRef<any> = null!;

        view0: EmbeddedViewRef<any> = null!;
        view1: EmbeddedViewRef<any> = null!;
        view2: EmbeddedViewRef<any> = null!;
        view3: EmbeddedViewRef<any> = null!;

        ngAfterViewInit() {
          // insert at the front
          this.view1 = this.container.createEmbeddedView(this.projection);

          // insert at the front again
          this.view0 = this.container.createEmbeddedView(this.projection, {}, 0);

          // insert at the end
          this.view3 = this.container.createEmbeddedView(this.projection);

          // insert in the middle
          this.view2 = this.container.createEmbeddedView(this.projection, {}, 2);
        }
      }

      @Component({
        template: `
          <comp>test</comp>
        `,
        standalone: false,
      })
      class App {}

      TestBed.configureTestingModule({
        declarations: [App, Comp],
      });
      const fixture = TestBed.createComponent(App);
      fixture.detectChanges();
      const comp = fixture.debugElement.query(By.directive(Comp)).injector.get(Comp);

      expect(comp.container.indexOf(comp.view0)).toBe(0);
      expect(comp.container.indexOf(comp.view1)).toBe(1);
      expect(comp.container.indexOf(comp.view2)).toBe(2);
      expect(comp.container.indexOf(comp.view3)).toBe(3);

      // Both ViewEngine and Ivy only honor one of the inserted ng-content components, even though
      // all are inserted.
      expect(fixture.nativeElement.textContent).toBe('test');
    });
  });

  describe('of another container like ngIf', () => {
    it('should insert into an empty container, at the front, in the middle, and at the end', () => {
      @Component({
        template: `
                  <ng-template #subContainer>
test
</ng-template>

                `,
        standalone: false,
      })
      class App {
        @ViewChild('container', {read: ViewContainerRef}) container: ViewContainerRef = null!;

        @ViewChild('subContainer', {read: TemplateRef}) subContainer: TemplateRef<any> = null!;

        view0: EmbeddedViewRef<any> = null!;
        view1: EmbeddedViewRef<any> = null!;
        view2: EmbeddedViewRef<any> = null!;
        view3: EmbeddedViewRef<any> = null!;

        constructor(public changeDetectorRef: ChangeDetectorRef) {}

        ngAfterViewInit() {
          // insert at the front
          this.view1 = this.container.createEmbeddedView(this.subContainer, null, 0);

          // insert at the front again
          this.view0 = this.container.createEmbeddedView(this.subContainer, null, 0);

          // insert at the end
          this.view3 = this.container.createEmbeddedView(this.subContainer, null, 2);

          // insert in the middle
          this.view2 = this.container.createEmbeddedView(this.subContainer, null, 2);

          // We need to run change detection here to avoid
          // ExpressionChangedAfterItHasBeenCheckedError because of the value getting passed to ngIf
          // in the template.
          this.changeDetectorRef.detectChanges();
        }
      }

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

      expect(app.container.indexOf(app.view0)).toBe(0);
      expect(app.container.indexOf(app.view1)).toBe(1);
      expect(app.container.indexOf(app.view2)).toBe(2);
      expect(app.container.indexOf(app.view3)).toBe(3);

      expect(fixture.debugElement.queryAll(By.css('div.dynamic')).length).toBe(4);
    });
  });


describe('before component view', () => {
      @Directive({
        selector: '[viewInserting]',
        exportAs: 'vi',
        standalone: false,
      })
      class ViewInsertingDir {
        constructor(private _vcRef: ViewContainerRef) {}

        insert(beforeView: ViewRef, insertTpl: TemplateRef<{}>) {
          this._vcRef.insert(beforeView, 0);
          this._vcRef.createEmbeddedView(insertTpl, {}, 0);
        }
      }

      @Component({
        selector: 'dynamic-cmpt',
        template: '|before',
        standalone: false,
      })
      class DynamicComponent {}

      it('should insert in front a dynamic component view', () => {
        @Component({
          selector: 'test-cmpt',
          template: `
                <ng-template #insert>insert</ng-template>

<ng-template #vi="vi" viewInserting></ng-template>

              `,
          standalone: false,
        })
        class TestCmpt {
          @ViewChild('insert', {static: true}) insertTpl!: TemplateRef<{}>;
          @ViewChild('vi', {static: true}) viewInsertingDir!: ViewInsertingDir;

          constructor(
            private _vcr: ViewContainerRef,
            private _injector: Injector,
          ) {}

          insert() {
            // create a dynamic component view to act as an "insert before" view
            const beforeView = this._vcr.createComponent(DynamicComponent, {
              injector: this._injector,
            }).hostView;
            // change-detect the "before view" to create all child views
            beforeView.detectChanges();
            this.viewInsertingDir.insert(beforeView, this.insertTpl);
          }
        }

        TestBed.configureTestingModule({
          declarations: [TestCmpt, ViewInsertingDir, DynamicComponent],
        });

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

        fixture.componentInstance.insert();
        fixture.detectChanges();

        expect(fixture.nativeElement.textContent).toBe('insert|before');
      });
    });
  });

  describe('non-regression', () => {
    // https://github.com/angular/angular/issues/31971
    it('should insert component views into ViewContainerRef injected by querying <ng-container>', () => {
      @Component({
        selector: 'dynamic-cmpt',
        template: 'dynamic',
        standalone: false,
      })
      class DynamicComponent {}

      @Component({
        selector: 'app-root',
        template: `

start|

            <ng-container #container></ng-container>

|end

|click

        `,
        standalone: false,
      })
      class AppComponent {
        @ViewChild('container', {read: ViewContainerRef, static: true}) vcr!: ViewContainerRef;

        click() {
          this.vcr.createComponent(DynamicComponent);
        }
      }

      TestBed.configureTestingModule({
        declarations: [AppComponent, DynamicComponent],
      });
      const fixture = TestBed.createComponent(AppComponent);
      fixture.detectChanges();
      expect(fixture.nativeElement.textContent).toBe('start||end|click');

      fixture.componentInstance.click();
      fixture.detectChanges();
      expect(fixture.nativeElement.textContent).toBe('start|dynamic|end|click');
    });

    // https://github.com/angular/angular/issues/33679
    it('should insert embedded views into ViewContainerRef injected by querying <ng-container>', () => {
      @Component({
        selector: 'app-root',
        template: `

container start|

        <ng-container #container></ng-container>

|container end

        <ng-template #template >test</ng-template>

|click

        `,
        standalone: false,
      })
      class AppComponent {
        @ViewChild('container', {read: ViewContainerRef, static: true}) vcr!: ViewContainerRef;

        @ViewChild('template', {read: TemplateRef, static: true}) template!: TemplateRef<any>;

        click() {
          this.vcr.createEmbeddedView(this.template, undefined, 0);
        }
      }

      TestBed.configureTestingModule({
        declarations: [AppComponent],
      });
      const fixture = TestBed.createComponent(AppComponent);
      fixture.detectChanges();
      expect(fixture.nativeElement.textContent).toBe('container start||container end|click');

      fixture.componentInstance.click();
      fixture.detectChanges();
      expect(fixture.nativeElement.textContent).toBe('container start|test|container end|click');
    });

    it('should properly insert before views in a ViewContainerRef injected on ng-container', () => {
      @Component({
        selector: 'app-root',
        template: `
          <ng-template #parameterListItem let-parameter="parameter">
            {{parameter}}
          </ng-template>
          <ng-container *ngFor="let parameter of items;"
            [ngTemplateOutlet]="parameterListItem"
            [ngTemplateOutletContext]="{parameter:parameter}">
          </ng-container>
        `,
        standalone: false,
      })
      class AppComponent {
        items = [1];
      }

      TestBed.configureTestingModule({
        declarations: [AppComponent],
        imports: [CommonModule],
      });

      const fixture = TestBed.createComponent(AppComponent);
      fixture.detectChanges();
      expect(fixture.nativeElement.textContent.trim()).toContain('1');

      fixture.componentInstance.items = [2, 1];
      fixture.detectChanges();

      expect(fixture.nativeElement.textContent.trim()).toContain('2  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 {DOCUMENT, NgIf} from '@angular/common';
import {
  ApplicationRef,
  Component,
  ComponentRef,
  createComponent,
  createEnvironmentInjector,
  Directive,
  ElementRef,
  EmbeddedViewRef,
  EnvironmentInjector,
  forwardRef,
  inject,
  Injectable,
  InjectionToken,
  Injector,
  input,
  Input,
  NgModule,
  OnDestroy,
  reflectComponentType,
  Renderer2,
  Type,
  ViewChild,
  ViewContainerRef,
  ViewEncapsulation,
  ɵsetClassDebugInfo,
  ɵsetDocument,
  ɵɵdefineComponent,
} from '@angular/core';
import {stringifyForError} from '@angular/core/src/render3/util/stringify_utils';
import {TestBed} from '@angular/core/testing';
import {expect} from '@angular/platform-browser/testing/src/matchers';

import {global} from '../../src/util/global';

describe('component', () => {
  describe('view destruction', () => {
    it('should invoke onDestroy only once when a component is registered as a provider', () => {
      const testToken = new InjectionToken<ParentWithOnDestroy>('testToken');
      let destroyCalls = 0;

      @Component({
        selector: 'comp-with-on-destroy',
        template: '',
        providers: [{provide: testToken, useExisting: ParentWithOnDestroy}],
        standalone: false,
      })
      class ParentWithOnDestroy {
        ngOnDestroy() {
          destroyCalls++;
        }
      }

      @Component({
        selector: 'child',
        template: '',
        standalone: false,
      })
      class ChildComponent {
        // We need to inject the parent so the provider is instantiated.
        constructor(_parent: ParentWithOnDestroy) {}
      }

      @Component({
        template: `
          <comp-with-on-destroy>
            <child></child>
          </comp-with-on-destroy>
        `,
        standalone: false,
      })
      class App {}

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

      expect(destroyCalls).toBe(1, 'Expected `ngOnDestroy` to only be called once.');
    });
  });

  it('should be able to dynamically insert a component into a view container at the root of a component', () => {
    @Component({
      template: 'hello',
      standalone: false,
    })
    class HelloComponent {}

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

    @Component({
      template: `
            <wrapper>

            </wrapper>
          `,
      standalone: false,
    })
    class App {
      @ViewChild('insertionPoint', {read: ViewContainerRef}) viewContainerRef!: ViewContainerRef;
    }

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

    const instance = fixture.componentInstance;
    instance.viewContainerRef.createComponent(HelloComponent);

    expect(fixture.nativeElement.textContent.trim()).toBe('hello');
  });

  it('should not throw when calling `detectChanges` on the ChangeDetectorRef of a destroyed view', () => {
    @Component({
      template: 'hello',
      standalone: false,
    })
    class HelloComponent {}

    @Component({
      template: `<div #insertionPoint></div>`,
      standalone: false,
    })
    class App {
      @ViewChild('insertionPoint', {read: ViewContainerRef}) viewContainerRef!: ViewContainerRef;
    }

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

    const componentRef = fixture.componentInstance.viewContainerRef.createComponent(HelloComponent);
    fixture.detectChanges();

    expect(() => {
      componentRef.destroy();
      componentRef.changeDetectorRef.detectChanges();
    }).not.toThrow();
  });

  // TODO: add tests with Native once tests run in real browser (domino doesn't support shadow root)
  describe('encapsulation', () => {
    @Component({
      selector: 'wrapper',
      encapsulation: ViewEncapsulation.None,
      template: `<encapsulated></encapsulated>`,
      standalone: false,
    })
    class WrapperComponent {}

    @Component({
      selector: 'encapsulated',
      encapsulation: ViewEncapsulation.Emulated,
      // styles must be non-empty to trigger `ViewEncapsulation.Emulated`
      styles: `:host {display: block}`,
      template: `foo<leaf></leaf>`,
      standalone: false,
    })
    class EncapsulatedComponent {}

    @Component({
      selector: 'leaf',
      encapsulation: ViewEncapsulation.None,
      template: `<span>bar</span>`,
      standalone: false,
    })
    class LeafComponent {}

    beforeEach(() => {
      TestBed.configureTestingModule({
        declarations: [WrapperComponent, EncapsulatedComponent, LeafComponent],
      });
    });

    it('should encapsulate children, but not host nor grand children', () => {
      const fixture = TestBed.createComponent(WrapperComponent);
      fixture.detectChanges();
      expect(fixture.nativeElement.innerHTML).toMatch(
        /<encapsulated _nghost-[a-z\-]+(\d+)="">foo<leaf _ngcontent-[a-z\-]+\1="">bar<\/span><\/leaf><\/encapsulated>/,
      );
    });

    it('should encapsulate host', () => {
      const fixture = TestBed.createComponent(EncapsulatedComponent);
      fixture.detectChanges();
      const html = fixture.nativeElement.outerHTML;
      const match = html.match(/_nghost-([a-z\-]+\d+)/);
      expect(match).toBeDefined();
      expect(html).toMatch(new RegExp(`<leaf _ngcontent-${match[1]}=""><span>bar</span></leaf>`));
    });

    it('should encapsulate host and children with different attributes', () => {
      // styles must be non-empty to trigger `ViewEncapsulation.Emulated`
      TestBed.overrideComponent(LeafComponent, {
        set: {encapsulation: ViewEncapsulation.Emulated, styles: [`span {color:red}`]},
      });
      const fixture = TestBed.createComponent(EncapsulatedComponent);
      fixture.detectChanges();
      const html = fixture.nativeElement.outerHTML;
      const match = html.match(/_nghost-([a-z\-]+\d+)/g);
      expect(match).toBeDefined();
      expect(match.length).toEqual(2);
      expect(html).toMatch(
        `<leaf ${match[0].replace('_nghost', '_ngcontent')}="" ${
          match[1]
        }="">bar</leaf>
`,
      );
    });

    it('should be off for a component with no styles', () => {
      TestBed.overrideComponent(EncapsulatedComponent, {
        set: {styles: undefined},
      });
      const fixture = TestBed.createComponent(EncapsulatedComponent);
      fixture.detectChanges();
      const html = fixture.nativeElement.outerHTML;
      expect(html).not.toContain('<encapsulated _nghost-');
      expect(html).not.toContain('<leaf _ngcontent-');
    });

    it('should be off for a component with empty styles', () => {
      TestBed.overrideComponent(EncapsulatedComponent, {
        set: {styles: [`  `, '', '/*comment*/']},
      });
      const fixture = TestBed.createComponent(EncapsulatedComponent);
      fixture.detectChanges();
      const html = fixture.nativeElement.outerHTML;
      expect(html).not.toContain('<encapsulated _nghost-');
      expect(html).not.toContain('<leaf _ngcontent-');
    });
  });

  describe('view destruction', () => {
    it('should invoke onDestroy when directly destroying a root view', () => {
      let wasOnDestroyCalled = false;

      @Component({
        selector: 'comp-with-destroy',
        template: ``,
        standalone: false,
      })
      class ComponentWithOnDestroy implements OnDestroy {
        ngOnDestroy() {
          wasOnDestroyCalled = true;
        }
      }

      // This test asserts that the view tree is set up correctly based on the knowledge that this
      // tree is used during view destruction. If the child view is not correctly attached as a
      // child of the root view, then the onDestroy hook on the child view will never be called
      // when the view tree is torn down following the destruction of that root view.
      @Component({
        selector: `test-app`,
        template: `<comp-with-destroy></comp-with-destroy>`,
        standalone: false,
      })
      class TestApp {}

      TestBed.configureTestingModule({declarations: [ComponentWithOnDestroy, TestApp]});
      const fixture = TestBed.createComponent(TestApp);
      fixture.detectChanges();
      fixture.destroy();
      expect(wasOnDestroyCalled).toBe(
        true,
        'Expected component onDestroy method to be called when its parent view is destroyed',
      );
    });
  });


it('should handle nullish coalescing inside templates', () => {
    @Component({
      template: `

          Hello, {{ firstName ?? 'Frodo' }}!
          You are a Balrog: {{ falsyValue ?? true }}

      `,
      standalone: false,
    })
    class App {
      firstName: string | null = null;
      lastName: string | null = null;
      lastNameFallback = 'Baggins';
      falsyValue = false;
    }

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

    expect(content).toContain('Hello, Frodo!');
    expect(content).toContain('You are a Balrog: false');
    expect(content).toContain(`<span title="Your last name is Baggins">`);
  });

  it('should handle safe keyed reads inside templates', () => {
    @Component({
      template: `

        Hello, {{ knownNames?.[0]?.[1] }}!
        You are a Balrog: {{ species?.[0]?.[1]?.[2]?.[3]?.[4]?.[5] || 'unknown' }}
        You are an Elf: {{ speciesMap?.[keys?.[0] ?? 'key'] }}
        You are an Orc: {{ speciesMap?.['key'] }}

    `,
      standalone: false,
    })
    class App {
      unknownNames: string[] | null = null;
      knownNames: string[][] = [['Frodo', 'Bilbo']];
      species = null;
      keys = null;
      speciesMap: Record<string, string> = {key: 'unknown'};
    }

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

    expect(content).toContain('Hello, Bilbo!');
    expect(content).toContain('You are a Balrog: unknown');
    expect(content).toContain('You are an Elf: unknown');
    expect(content).toContain(`<span title="Your last name is unknown">`);
  });

  it('should handle safe keyed reads inside templates', () => {
    @Component({
      template: `

          Hello, {{ person.getName?.() }}!
          You are a Balrog: {{ person.getSpecies?.()?.()?.()?.()?.() || 'unknown' }}

      `,
      standalone: false,
    })
    class App {
      person: {
        getName: () => string;
        getLastName?: () => string;
        getSpecies?: () => () => () => () => () => string;
      } = {getName: () => 'Bilbo'};
    }

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

    expect(content).toContain('Hello, Bilbo!');
    expect(content).toContain('You are a Balrog: unknown');
    expect(content).toContain(`<span title="Your last name is unknown">`);
  });

  it('should not invoke safe calls more times than plain calls', () => {
    const returnValue = () => () => () => () => 'hi';
    let plainCalls = 0;
    let safeCalls = 0;

    @Component({
      template: `{{ safe?.()?.()?.()?.()?.() }} {{ plain()()()()() }}`,
      standalone: false,
    })
    class App {
      plain() {
        plainCalls++;
        return returnValue;
      }

      safe() {
        safeCalls++;
        return returnValue;
      }
    }

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

    expect(safeCalls).toBeGreaterThan(0);
    expect(safeCalls).toBe(plainCalls);
  });

  it('should handle nullish coalescing inside host bindings', () => {
    const logs: string[] = [];

    @Directive({
      selector: '[some-dir]',
      host: {
        '[attr.first-name]': `'Hello, ' + (firstName ?? 'Frodo') + '!'`,
        '(click)': `logLastName(lastName ?? lastNameFallback ?? 'unknown')`,
      },
      standalone: false,
    })
    class Dir {
      firstName: string | null = null;
      lastName: string | null = null;
      lastNameFallback = 'Baggins';

      logLastName(name: string) {
        logs.push(name);
      }
    }

    @Component({
      template: `<button some-dir>Click me</button>`,
      standalone: false,
    })
    class App {}

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

    expect(button.getAttribute('first-name')).toBe('Hello, Frodo!');
    expect(logs).toEqual(['Baggins']);
  });

  it('should render SVG nodes placed inside ng-template', () => {
    @Component({
      template: `
        <svg>
          <ng-template [ngIf]="condition">
            <text>Hello</text>
          </ng-template>
        </svg>
      `,
      standalone: false,
    })
    class MyComp {
      condition = true;
    }

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

    expect(fixture.nativeElement.innerHTML).toContain('<text>Hello</text>');
  });

  it('should handle shorthand property declarations in templates', () => {
    @Directive({
      selector: '[my-dir]',
      standalone: false,
    })
    class Dir {
      @Input('my-dir') value: any;
    }

    @Component({
      template: `<div [my-dir]="{a, b: 2, someProp}"></div>`,
      standalone: false,
    })
    class App {
      @ViewChild(Dir) directive!: Dir;
      a = 1;
      someProp = 3;
    }

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

    expect(fixture.componentInstance.directive.value).toEqual({a: 1, b: 2, someProp: 3});
  });

  it('should handle numeric separators in templates', () => {
    @Component({
      template: 'Balance: ${{ 1_000_000 * multiplier }}',
      standalone: false,
    })
    class App {
      multiplier = 5;
    }

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

    expect(fixture.nativeElement.textContent).toBe('Balance: $5000000');
  });

  it('should handle calls to a safe access in templates', () => {
    @Component({
      template: `
      Hello, {{ (person?.getName() || 'unknown') }}!
    `,
      standalone: false,
    })
    class App {
      person = null;
    }

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

    expect(fixture.nativeElement.textContent).toContain('Hello, unknown!');
  });


describe('and declaration component is CheckAlways', () => {
      beforeEach(() => {
        fixture.componentInstance.showCheckAlwaysDeclare = true;
        fixture.componentInstance.showOnPushInsert = true;
        fixture.detectChanges(false);
        viewExecutionLog.length = 0;
      });

      it('should set up the component under test correctly', () => {
        expect(viewExecutionLog.length).toEqual(0);
        expect(trim(fixture.nativeElement.textContent)).toEqual(
          'DeclareComp(world) OnPushInsertComp(Hello) Hello world!',
        );
      });

      it('should CD at insertion point only', () => {
        declareComp.name = 'Angular';
        fixture.detectChanges(false);
        expect(viewExecutionLog).toEqual(['Insert']);
        viewExecutionLog.length = 0;
        expect(trim(fixture.nativeElement.textContent)).toEqual(
          'DeclareComp(Angular) OnPushInsertComp(Hello) Hello Angular!',
          'Expect transplanted LView to be CD because the declaration is CD.',
        );

        onPushInsertComp.greeting = 'Hi';
        fixture.detectChanges(false);
        expect(viewExecutionLog).toEqual(['Insert']);
        viewExecutionLog.length = 0;
        expect(trim(fixture.nativeElement.textContent)).toEqual(
          'DeclareComp(Angular) OnPushInsertComp(Hello) Hello Angular!',
          'expect no change because it is on push.',
        );

        onPushInsertComp.changeDetectorRef.markForCheck();
        fixture.detectChanges(false);
        expect(viewExecutionLog).toEqual(['Insert']);
        viewExecutionLog.length = 0;
        expect(trim(fixture.nativeElement.textContent)).toEqual(
          'DeclareComp(Angular) OnPushInsertComp(Hi) Hi Angular!',
        );

        // Destroy insertion should also destroy declaration
        appComp.showOnPushInsert = false;
        fixture.detectChanges(false);
        expect(viewExecutionLog).toEqual([]);
        viewExecutionLog.length = 0;
        expect(trim(fixture.nativeElement.textContent)).toEqual('DeclareComp(Angular)');

        // Restore both
        appComp.showOnPushInsert = true;
        fixture.detectChanges(false);
        expect(viewExecutionLog).toEqual(['Insert']);
        viewExecutionLog.length = 0;
        expect(trim(fixture.nativeElement.textContent)).toEqual(
          'DeclareComp(Angular) OnPushInsertComp(Hello) Hello Angular!',
        );

        // Destroy declaration, But we should still be able to see updates in insertion
        appComp.showCheckAlwaysDeclare = false;
        onPushInsertComp.greeting = 'Hello';
        onPushInsertComp.changeDetectorRef.markForCheck();
        fixture.detectChanges(false);
        expect(viewExecutionLog).toEqual(['Insert']);
        viewExecutionLog.length = 0;
        expect(trim(fixture.nativeElement.textContent)).toEqual(
          'OnPushInsertComp(Hello) Hello Angular!',
        );
      });

      it('is not checked if detectChanges is called in declaration component', () => {
        declareComp.name = 'Angular';
        declareComp.changeDetector.detectChanges();
        expect(viewExecutionLog).toEqual([]);
        viewExecutionLog.length = 0;
        expect(trim(fixture.nativeElement.textContent)).toEqual(
          'DeclareComp(Angular) OnPushInsertComp(Hello) Hello world!',
        );
      });

      it('is checked as part of CheckNoChanges pass', () => {
        fixture.detectChanges(true);
        expect(viewExecutionLog).toEqual([
          'Insert',
          null /* logName set to null afterViewChecked */,
        ]);
        viewExecutionLog.length = 0;
        expect(trim(fixture.nativeElement.textContent)).toEqual(
          'DeclareComp(world) OnPushInsertComp(Hello) Hello world!',
        );
      });
    });

    describe('and declaration and insertion components are OnPush', () => {
      beforeEach(() => {
        fixture.componentInstance.showOnPushDeclare = true;
        fixture.componentInstance.showOnPushInsert = true;
        fixture.detectChanges(false);
        viewExecutionLog.length = 0;
      });

      it('should set up component under test correctly', () => {
        expect(viewExecutionLog.length).toEqual(0);
        expect(trim(fixture.nativeElement.textContent)).toEqual(
          'OnPushDeclareComp(world) OnPushInsertComp(Hello) Hello world!',
        );
      });

      it('should not check anything when no views are dirty', () => {
        fixture.detectChanges(false);
        expect(viewExecutionLog).toEqual([]);
      });

      it('should CD at insertion point only', () => {
        onPushDeclareComp.name = 'Angular';
        onPushInsertComp.greeting = 'Hi';
        // mark declaration point dirty
        onPushDeclareComp.changeDetector.markForCheck();
        fixture.detectChanges(false);
        expect(viewExecutionLog).toEqual(['Insert']);
        viewExecutionLog.length = 0;
        expect(trim(fixture.nativeElement.textContent)).toEqual(
          'OnPushDeclareComp(Angular) OnPushInsertComp(Hello) Hello Angular!',
        );

        // mark insertion point dirty
        onPushInsertComp.changeDetectorRef.markForCheck();
        fixture.detectChanges(false);
        expect(viewExecutionLog).toEqual(['Insert']);
        viewExecutionLog.length = 0;
        expect(trim(fixture.nativeElement.textContent)).toEqual(
          'OnPushDeclareComp(Angular) OnPushInsertComp(Hi) Hi Angular!',
        );

        // mark both insertion and declaration point dirty
        onPushInsertComp.changeDetectorRef.markForCheck();
        onPushDeclareComp.changeDetector.markForCheck();
        fixture.detectChanges(false);
        expect(viewExecutionLog).toEqual(['Insert']);
        viewExecutionLog.length = 0;
      });

      it('is checked if detectChanges is called in declaration component', () => {
        onPushDeclareComp.name = 'Angular';
        onPushDeclareComp.changeDetector.detectChanges();
        expect(trim(fixture.nativeElement.textContent)).toEqual(
          'OnPushDeclareComp(Angular) OnPushInsertComp(Hello) Hello world!',
        );
      });

      // TODO(FW-1774): blocked by https://github.com/angular/angular/pull/34443
      xit('is checked as part of CheckNoChanges pass', () => {
        // mark declaration point dirty
        onPushDeclareComp.changeDetector.markForCheck();
        fixture.detectChanges(false);
        expect(viewExecutionLog).toEqual([
          'Insert',
          null /* logName set to null in afterViewChecked */,
        ]);
        viewExecutionLog.length = 0;

        // mark insertion point dirty
        onPushInsertComp.changeDetectorRef.markForCheck();
        fixture.detectChanges(false);
        expect(viewExecutionLog).toEqual(['Insert', null]);
        viewExecutionLog.length = 0;

        // mark both insertion and declaration point dirty
        onPushInsertComp.changeDetectorRef.markForCheck();
        onPushDeclareComp.changeDetector.markForCheck();
        fixture.detectChanges(false);
        expect(viewExecutionLog).toEqual(['Insert', null]);
        viewExecutionLog.length = 0;
      });

      it('does not cause infinite change detection if transplanted view is dirty and destroyed before refresh', () => {
        // mark declaration point dirty
        onPushDeclareComp.changeDetector.markForCheck();
        // detach insertion so the transplanted view doesn't get refreshed when CD runs
        onPushInsertComp.changeDetectorRef.detach();
        // run CD, which will set the `RefreshView` flag on the transplanted view
        fixture.detectChanges(false);
        // reattach insertion so the DESCENDANT_VIEWS counters update
        onPushInsertComp.changeDetectorRef.reattach();
        // make it so the insertion is destroyed before getting refreshed
        fixture.componentInstance.showOnPushInsert = false;
        // run CD again. If we didn't clear the flag/counters when destroying the view, this
        // would cause an infinite CD because the counters will be >1 but we will never reach a
        // view to refresh and decrement the counters.
        fixture.detectChanges(false);
      });
    });
  });