Skip to content
This repository has been archived by the owner on Feb 26, 2024. It is now read-only.

fakeAsync tests not working with lodash debounce #1050

Closed
johnmcase opened this issue Mar 16, 2018 · 12 comments · Fixed by #1051
Closed

fakeAsync tests not working with lodash debounce #1050

johnmcase opened this issue Mar 16, 2018 · 12 comments · Fixed by #1051

Comments

@johnmcase
Copy link

I had originally posted this issue to Angular angular/angular#22826

@JiaLiPassion requested me to re-submit the issue here.

I'm submitting a...


[ ] Regression (a behavior that used to work and stopped working in a new release)
[ X ] Bug report  
[ ] Feature request
[ ] Documentation issue or request
[ ] Support request => Please do not submit support request here, instead see https://github.com/angular/angular/blob/master/CONTRIBUTING.md#question

Current behavior

Using fakeAsync() and tick() to test functions that are debounced with lodash.debounce() do not work as expected.

Expected behavior

fakeAsync() and tick() should be able to be used to synchronously test debounced functions.

Minimal reproduction of the problem with instructions

I have a repository with a minimal reproduction: https://github.com/johnmcase/angular-test-debounce
Just clone, install, ng test

What is the motivation / use case for changing the behavior?

Environment


Angular version: 4.4.3 in the project I found this in.  5.2.0 in minimal reproduction

Browser:
- [ X ] Chrome (desktop) version XX
- [ ] Chrome (Android) version XX
- [ ] Chrome (iOS) version XX
- [ ] Firefox version XX
- [ ] Safari (desktop) version XX
- [ ] Safari (iOS) version XX
- [ ] IE version XX
- [ ] Edge version XX
 
For Tooling issues:
- Node version: 6.12.0  
- Platform:  Windows 10 

Others:

@JiaLiPassion
Copy link
Collaborator

thanks , I will implement the feature here.

JiaLiPassion added a commit to JiaLiPassion/zone.js that referenced this issue Mar 17, 2018
JiaLiPassion added a commit to JiaLiPassion/zone.js that referenced this issue Mar 17, 2018
JiaLiPassion added a commit to JiaLiPassion/zone.js that referenced this issue Mar 30, 2018
JiaLiPassion added a commit to JiaLiPassion/zone.js that referenced this issue Mar 31, 2018
JiaLiPassion added a commit to JiaLiPassion/zone.js that referenced this issue Mar 31, 2018
JiaLiPassion added a commit to JiaLiPassion/zone.js that referenced this issue Mar 31, 2018
JiaLiPassion added a commit to JiaLiPassion/zone.js that referenced this issue Mar 31, 2018
JiaLiPassion added a commit to JiaLiPassion/zone.js that referenced this issue Mar 31, 2018
JiaLiPassion added a commit to JiaLiPassion/zone.js that referenced this issue Mar 31, 2018
mhevery pushed a commit that referenced this issue Mar 31, 2018
…akeAsync exit (#1051)

* chore: release v0.8.21

* fix(fakeAsync): fix #1050, work with fakeAsync
@ArielGueta
Copy link

Why it's closed? The problem still exists.

@ArielGueta
Copy link

/**  THE COMPONENT **/

@Component({
  template: `
    <button (click)="debounced()">Change</button>
    <p>{{name}}</p>
  `
})
export class ClickComponent implements OnInit {
  name = 'init';
  debounced = debounce(this.onClick.bind(this), 300);

  onClick() {
    this.name = 'changed';
  }

}

/**  THE TEST **/
it('should changed on click', fakeAsync(() => {
  const button = fixture.nativeElement.querySelector('button');
  button.click();
  tick(310);
  fixture.detectChanges();
  expect(fixture.nativeElement.querySelector('p').textContent).toEqual('changed');
}));


/**  THE REAL LODASH IMPLEMENTATION **/

function debounce(func, wait, options: any = false) {
  let lastArgs,
    lastThis,
    maxWait,
    result,
    timerId,
    lastCallTime

  let lastInvokeTime = 0
  let leading = false
  let maxing = false
  let trailing = true

  // Bypass `requestAnimationFrame` by explicitly setting `wait=0`.
  const useRAF = (!wait && wait !== 0 && typeof requestAnimationFrame === 'function')

  if (typeof func != 'function') {
    throw new TypeError('Expected a function')
  }
  wait = +wait || 0
  if (options) {
    leading = !!options.leading
    maxing = 'maxWait' in options
    maxWait = maxing ? Math.max(+options.maxWait || 0, wait) : maxWait
    trailing = 'trailing' in options ? !!options.trailing : trailing
  }

  function invokeFunc(time) {
    const args = lastArgs
    const thisArg = lastThis

    lastArgs = lastThis = undefined
    lastInvokeTime = time
    result = func.apply(thisArg, args)
    return result
  }

  function startTimer(pendingFunc, wait) {
    if (useRAF) {
      return requestAnimationFrame(pendingFunc)
    }
    return setTimeout(pendingFunc, wait)
  }

  function cancelTimer(id) {
    if (useRAF) {
      return cancelAnimationFrame(id)
    }
    clearTimeout(id)
  }

  function leadingEdge(time) {
    // Reset any `maxWait` timer.
    lastInvokeTime = time
    // Start the timer for the trailing edge.
    timerId = startTimer(timerExpired, wait)
    // Invoke the leading edge.
    return leading ? invokeFunc(time) : result
  }

  function remainingWait(time) {
    const timeSinceLastCall = time - lastCallTime
    const timeSinceLastInvoke = time - lastInvokeTime
    const timeWaiting = wait - timeSinceLastCall

    return maxing
      ? Math.min(timeWaiting, maxWait - timeSinceLastInvoke)
      : timeWaiting
  }

  function shouldInvoke(time) {
    const timeSinceLastCall = time - lastCallTime
    const timeSinceLastInvoke = time - lastInvokeTime

    // Either this is the first call, activity has stopped and we're at the
    // trailing edge, the system time has gone backwards and we're treating
    // it as the trailing edge, or we've hit the `maxWait` limit.
    return (lastCallTime === undefined || (timeSinceLastCall >= wait) ||
      (timeSinceLastCall < 0) || (maxing && timeSinceLastInvoke >= maxWait))
  }

  function timerExpired() {
    const time = Date.now()
    if (shouldInvoke(time)) {
      return trailingEdge(time)
    }
    // Restart the timer.
    timerId = startTimer(timerExpired, remainingWait(time))
  }

  function trailingEdge(time) {
    timerId = undefined

    // Only invoke if we have `lastArgs` which means `func` has been
    // debounced at least once.
    if (trailing && lastArgs) {
      return invokeFunc(time)
    }
    lastArgs = lastThis = undefined
    return result
  }

  function cancel() {
    if (timerId !== undefined) {
      cancelTimer(timerId)
    }
    lastInvokeTime = 0
    lastArgs = lastCallTime = lastThis = timerId = undefined
  }

  function flush() {
    return timerId === undefined ? result : trailingEdge(Date.now())
  }

  function pending() {
    return timerId !== undefined
  }

  function debounced(...args) {
    const time = Date.now()
    const isInvoking = shouldInvoke(time)

    lastArgs = args
    lastThis = this
    lastCallTime = time

    if (isInvoking) {
      if (timerId === undefined) {
        return leadingEdge(lastCallTime)
      }
      if (maxing) {
        // Handle invocations in a tight loop.
        timerId = startTimer(timerExpired, wait)
        return invokeFunc(lastCallTime)
      }
    }
    if (timerId === undefined) {
      timerId = startTimer(timerExpired, wait)
    }
    return result
  }
  debounced.cancel = cancel
  debounced.flush = flush
  debounced.pending = pending
  return debounced
}

@JiaLiPassion
Copy link
Collaborator

@ArielGueta, you need to import this file.

import 'zone.js/dist/zone-patch-rxjs-fake-async';

I have tested with your test code, it passed.

@ArielGueta
Copy link

Thanks for your response. I added this to the test.ts file (angular-cli):

import 'zone.js/dist/zone-testing';
import 'zone.js/dist/zone-patch-rxjs-fake-async';

And it's still not working. (zone.js version 0.8.26)

@JiaLiPassion
Copy link
Collaborator

@ArielGueta , I see, the angular side is not released yet, I used a local version to test.

you can use the following code for now. Please see the fakeAsync/tick import part.

import { TestBed } from '@angular/core/testing';
import { AppComponent } from './app.component';

declare let Zone: any;
const async = Zone[Zone.__symbol__('asyncTest')];
const { fakeAsync, tick } = Zone[Zone.__symbol__('fakeAsyncTest')];
describe('AppComponent', () => {
  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [AppComponent]
    }).compileComponents();
  }));

  it(
    'should changed on click',
    fakeAsync(() => {
      const fixture = TestBed.createComponent(AppComponent);
      const button = fixture.nativeElement.querySelector('#debounce');
      button.click();
      tick(310);
      fixture.detectChanges();
      expect(fixture.nativeElement.querySelector('p').textContent).toEqual('changed');
    })
  );

@ArielGueta
Copy link

Yes, it's working, Thanks! Can we reopen it until it will be available in Angular?

@JiaLiPassion
Copy link
Collaborator

@ArielGueta , it has been merged in Angular, you can try 6.0.0-rc5.

@ArielGueta
Copy link

We are on version 5. I think it should also be merged to version 5. Most of us are still in 5.

@JiaLiPassion
Copy link
Collaborator

@ArielGueta , I see, you can file an issue in angular/angular, I am not sure about the merge policy, but it is merged to v5 branch, I think your issue will be fixed. For now, you can use the fakeAsync/async from Zone instead of from angular.

@ArielGueta
Copy link

I will. Thanks!

@JiaLiPassion
Copy link
Collaborator

@mhevery , do you think we should also merge angular/angular#23108 and angular/angular#23227 into angular v5 branch? So user who use angular v5 can get the new features of

  • fakeAsync/Date.now support
  • fakeAsync/rxjs scheduler support

Please review, thank you.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
3 participants