cypress-io / cypress

Fast, easy and reliable testing for anything that runs in a browser.
https://cypress.io
MIT License
46.41k stars 3.14k forks source link

Custom interval breaks change detection for cypress #28938

Open muhamedkarajic opened 4 months ago

muhamedkarajic commented 4 months ago

Current behavior

When I have a component and that component uses a custom interval which in the below example is called Tick. Once that tick is used in any service cypress is forever awaiting the changes. I assume it forever awaits the function stack to be empty.

Desired behavior

I wonder if cypress could maybe somehow detect such things and give a warrning rather then just not render my component which injects the service which then uses the tick. Ofc I can overcome the issue my making this a factory (service) which will provide me a special Tick when its a testing enviroment.

Test code to reproduce

Tick:

export class Tick {
    onTick$ = new ReplaySubject<void>(1);

    constructor(
        private isSkippingTick: () => boolean, 
        public delayInMilliseconds = 1_000, 
        private onDestroy$: Subject<void> | null = null,
        private onTick: (() => unknown) | null = null
    ) {
        this.tryToTick();
        if(onTick)
            this.onTick$.subscribe(() => onTick())
    }

    private tryToTick() {
        asyncScheduler.schedule(() => {

            if (this.isSkippingTick()) {
                this.scheduleTick(); // --> to fix remove the infinite function stack
                return;
            }

            this.onTick$.next();
            this.scheduleTick(); // --> to fix remove the infinite function stack
        })
    }

    scheduleTick() {
        let tick = timer(this.delayInMilliseconds).pipe(
            observeOn(asyncScheduler));

        if (this.onDestroy$) {
            tick = tick.pipe(
                takeUntil(this.onDestroy$)
            )
        }
        tick.subscribe(() => this.tryToTick());
    }
}

Service:

@Injectable({providedIn: 'root'})
export class ExampleService {
    tick = new Tick(() => false, 16, null, () => { /* Any function which you wish to execute after some delay. */});
}

Component:

@Component({
  standalone: true,
  template: `<div id="example-div">I'm not visible. {{ (condition$ | async) ? "Neither I am." : 'I will anyways never be visible.' }}</div>`,
  imports: [CommonModule],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ExampleComponent {
  exampleService = inject(ExampleService); // -> comment this out then it will work.
  condition$ = new BehaviorSubject<boolean>(true);
}

Cypress code:

describe('Example', () => {
    it('div is properly rendering', () => {
        cy.viewport(900, 500).mount(ExampleComponent).then(_env => {
            cy
                .get('#example-div')
                .should('be.visible')
                .then(() => {
                    console.log('Finished...');
                });
        })
    })
});

Cypress Version

13.6.4

Node version

v18.16.0

Operating System

macOS 12.6.6

Debug Logs

Chrome version: 121.0.6167.160

Other

Please note its not good enough to simply use isSkippingTick cause it still will schedule to reexecute tryToTick. It really has to be so that there is none macro task scheduled in the que.

jennifer-shehane commented 4 months ago

@muhamedkarajic Could you give an example of Cypress running against this component?

muhamedkarajic commented 4 months ago

@muhamedkarajic Could you give an example of Cypress running against this component?

I'll try to do one tomorrow. Its production code and the component is quit the component. However it should be possible to reproduce this. Ty for the quick response.

muhamedkarajic commented 4 months ago

@jennifer-shehane I have added for you a minimal demo in the initial comment.

rainerhahnekamp commented 4 months ago

I can confirm that I've experienced the same behavior.

muhamedkarajic commented 2 months ago

@jennifer-shehane this is only in the component testing environment. Any updates? In case of any questions I will gladly answer them.