Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Angular component test is not triggering autoDetectChanges #26990

Closed
NicolasCaous opened this issue Jun 9, 2023 · 7 comments
Closed

Angular component test is not triggering autoDetectChanges #26990

NicolasCaous opened this issue Jun 9, 2023 · 7 comments
Labels
CT Issue related to component testing

Comments

@NicolasCaous
Copy link

NicolasCaous commented Jun 9, 2023

Current behavior

I encountered the following situation: I had to test what would happen when a component that uses observables as input changed data. Here is the component:

class MyService {
  getData(): Observable<boolean> {
    return of(true); // Not important, is going to get mocked
  }
}

@Component({
  selector: 'my-component',
  template: `<ng-container *ngIf="vm$ | async as data">{{ data.myData ? 'true' : 'false' }}</ng-container>`,
  styles: ['.my-class {}'],
})
export class MyComponent {
  myData$: Observable<boolean> = this.myService.getData();

  vm$ = combineLatest({
    myData: this.myData$,
  }); // I'm using the VM pattern: https://medium.com/@maxmumford/use-the-view-model-pattern-for-more-reactive-angular-programming-507751a8349e

  constructor(private myService: MyService) {}
}

Here is the test:

describe('My Component', () => {
  let inputDisabled$: ReplaySubject<boolean>;

  beforeEach(() => {
    inputDisabled$ = new ReplaySubject<boolean>(1);
    inputDisabled$.next(true);

    cy.mount('<my-component></my-component>', {
      declarations: [MyComponent],
      imports: [NoopAnimationsModule],
      providers: [
        {
          provide: MyService,
          useValue: {
            getData: () => inputDisabled$.asObservable().pipe(tap(console.log)),
          } as Partial<MyService>,
        },
      ],
      autoDetectChanges: true,
    })
  });

  it('Should show "false" before data loads and "true" after', () => {
    cy.then(() => inputDisabled$.next(false));
    cy.get('my-component').should('have.text', 'false');

    cy.then(() => inputDisabled$.next(true));
    cy.get('my-component').should('have.text', 'true');
  });
});

However, when inputDisabled emits a new value for the second time, the data detection cycle is not starting. Resulting in a broken stale state. The logs show that RxJS processed the data correctly:

image

However, the result rendered on the DOM is not the latest value emitted

image

After manually triggering the change detection, it works:

describe('My Component Fixed', () => {
  let inputDisabled$: ReplaySubject<boolean>;
  let componentFixture: ComponentFixture<unknown>;

  beforeEach(() => {
    inputDisabled$ = new ReplaySubject<boolean>(1);
    inputDisabled$.next(true);

    cy.mount('<my-component></my-component>', {
      declarations: [MyComponent],
      imports: [NoopAnimationsModule],
      providers: [
        {
          provide: MyService,
          useValue: {
            getData: () => inputDisabled$.asObservable().pipe(tap(console.log)),
          } as Partial<MyService>,
        },
      ],
      autoDetectChanges: true,
    }).then((wrapper) => {
      componentFixture = wrapper.fixture;
    });
  });

  it('Should show "false" before data loads and "true" after', () => {
    cy.then(() => inputDisabled$.next(false));
    cy.then(() => componentFixture.detectChanges());
    cy.get('my-component').should('have.text', 'false');

    cy.then(() => inputDisabled$.next(true));
    cy.then(() => componentFixture.detectChanges());
    cy.get('my-component').should('have.text', 'true');
  });
});

image
image

Desired behavior

autoDetectChanges should work

Test code to reproduce

import { combineLatest, Observable, of, ReplaySubject } from 'rxjs';
import { Component } from '@angular/core';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { tap } from 'rxjs/operators';
import { ComponentFixture } from '@angular/core/testing';

class MyService {
  getData(): Observable<boolean> {
    return of(true); // Not important, is going to get mocked
  }
}

@Component({
  selector: 'my-component',
  template: `<ng-container *ngIf="vm$ | async as data">{{ data.myData ? 'true' : 'false' }}</ng-container>`,
  styles: ['.my-class {}'],
})
export class MyComponent {
  myData$: Observable<boolean> = this.myService.getData();

  vm$ = combineLatest({
    myData: this.myData$,
  }); // I'm using the VM pattern: https://medium.com/@maxmumford/use-the-view-model-pattern-for-more-reactive-angular-programming-507751a8349e

  constructor(private myService: MyService) {}
}

describe('My Component', () => {
  let inputDisabled$: ReplaySubject<boolean>;

  beforeEach(() => {
    inputDisabled$ = new ReplaySubject<boolean>(1);
    inputDisabled$.next(true);

    cy.mount('<my-component></my-component>', {
      declarations: [MyComponent],
      imports: [NoopAnimationsModule],
      providers: [
        {
          provide: MyService,
          useValue: {
            getData: () => inputDisabled$.asObservable().pipe(tap(console.log)),
          } as Partial<MyService>,
        },
      ],
      autoDetectChanges: true,
    })
  });

  it('Should show "false" before data loads and "true" after', () => {
    cy.then(() => inputDisabled$.next(false));
    cy.get('my-component').should('have.text', 'false');

    cy.then(() => inputDisabled$.next(true));
    cy.get('my-component').should('have.text', 'true');
  });
});


describe('My Component Fixed', () => {
  let inputDisabled$: ReplaySubject<boolean>;
  let componentFixture: ComponentFixture<unknown>;

  beforeEach(() => {
    inputDisabled$ = new ReplaySubject<boolean>(1);
    inputDisabled$.next(true);

    cy.mount('<my-component></my-component>', {
      declarations: [MyComponent],
      imports: [NoopAnimationsModule],
      providers: [
        {
          provide: MyService,
          useValue: {
            getData: () => inputDisabled$.asObservable().pipe(tap(console.log)),
          } as Partial<MyService>,
        },
      ],
      autoDetectChanges: true,
    }).then((wrapper) => {
      componentFixture = wrapper.fixture;
    });
  });

  it('Should show "false" before data loads and "true" after', () => {
    cy.then(() => inputDisabled$.next(false));
    cy.then(() => componentFixture.detectChanges());
    cy.get('my-component').should('have.text', 'false');

    cy.then(() => inputDisabled$.next(true));
    cy.then(() => componentFixture.detectChanges());
    cy.get('my-component').should('have.text', 'true');
  });
});

Cypress Version

v12.14.0

Node version

v18.10.0

Operating System

windows 10

Debug Logs

No response

Other

No response

@nagash77 nagash77 added the CT Issue related to component testing label Jun 12, 2023
@warrensplayer
Copy link
Contributor

@NicolasCaous Are you able to create reproducible example of the issue you're encountering with the code you included above? Here are some tips for providing a Short, Self Contained, Correct, Example and our own Troubleshooting Cypress guide.

Forking Cypress Test Tiny makes sharing a reproducible example easier to share and easier for our engineers to replicate your issues. This method also keeps the reproduction as simple as possible, which helps us eliminate potential causes and noise from the investigation.

That would be a big help to get us to be able to reproduce your issue the quickest. Thanks!

@NicolasCaous
Copy link
Author

Hi @warrensplayer, I need to wait for #27030 before doing that. I tried doing it now, but Cypress kept throwing errors for angular 16.1.0.

@mike-plummer
Copy link
Contributor

Hey @NicolasCaous , just wanted to let you know that Angular 16.1.0 support was released yesterday in Cypress 12.16.0. Looking forward to a reproduction case whenever you have a few minutes to upgrade and take a look

@mike-plummer
Copy link
Contributor

Unfortunately we have to close this issue due to inactivity. Please comment if there is new information to provide concerning the original issue and we can reopen.

@mike-plummer mike-plummer closed this as not planned Won't fix, can't repro, duplicate, stale Jul 5, 2023
@muhamedkarajic
Copy link

I have issues with cypress I just don't understand how to await something when its about an observable. I get the points when its about UI but what if I have a subject which I want to await for the next object or I have just a 'store' which I expect a value to be. It seems like somehow the async code executes after the sync.

@nagash77
Copy link
Contributor

@muhamedkarajic Issues in repo are reserved for bug reports and feature requests. Check out the Cypress Discord Community Discord chat, it can be helpful for debugging or answering questions on how to use Cypress.

@AronHoxha
Copy link

AronHoxha commented Jan 23, 2024

I would like to reopen this issue. I have created a reproducible fork of the cypress-test-tiny repo here.
Personally I have experienced this when utilizing angular routing, but in general the issue seems to be with using the async pipe

Edit: It seems that it is required to detect changes after navigation, this causes observable changes to update properly

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
CT Issue related to component testing
Projects
None yet
Development

No branches or pull requests

6 participants