Cypress spy working inconsistently between tests - cypress

I am spying on calls to fillText in a canvas. My component updates every 1000ms. Here's how I set things up:
In before, I send data that renders the canvas.
In beforeEach, I get the context and set the spy to the textSpy variable, which is captured by all the convenience methods.
In the test, I call convenience methods which:
send some new data (let's say it's a Series with the series number 1)
wait until the canvas has drawn (by waiting for "Series 1" which is also rendered as html)
assert that the spy has been called with text corresponding to the length and id number of the series.
The convenience methods are maybe a little all over the place/hard to follow so I hope my explanation helped:
describe('Displaying series after HScroll updates', () => {
const sendSeriesUpTo = (len: number, interval = 0.02) => {
// creates a series of the given length with values the given interval apart
// and sends that series
};
before(setupNonReloadingTests);
after(tearDownNonReloadingTests);
describe('data updates', () => {
let textSpy: SinonSpy;
const getSpy = () => cy.context('series').then(context => textSpy = cy.spy(context, 'fillText'));
// pass in an array of strings that must all have been called, but don't have to be the only calls
const expectTextCall = (textArray: string[]) => {
const textCalls = textSpy.getCalls().map(c => c.args[0]);
textArray.forEach(text => expect(textCalls).to.include(text));
};
const sendAndExpectFrameCount = (frameCount: number, series: string[] = ['1'], allSeries: string = '1') => {
sendSeriesUpTo(frameCount);
cy.contains(`Series: ${ allSeries }`).then(() => {
const arr = [`(${ frameCount } frames)`];
series.forEach(s => arr.push(`SERIES ${ s }`));
expectTextCall(arr);
});
};
const sendAndExpectRange = (from: number, to: number) => {
const diff = to - from + 1;
const array = [...Array(diff).keys()];
array.forEach(n => {
sendAndExpectFrameCount(n + from);
});
};
describe('receiving a single non-overflowing series', () => {
before(() => {
cy.sendStudyUpdateWithSeriesArray([1, 2, 3].map(n => createFdsObject()));
});
beforeEach(() => {
cy.context('series').then(context => textSpy = cy.spy(context, 'fillText'));
});
describe('before overflowing', () => {
it('displays the initial series, ' +
'displays non-overflowing updates to the series', () => {
sendAndExpectRange(80, 85);
});
});
describe('overflowing', () => {
it('scrolls to the end (most current) when a frame is added to overflow the screen', () => {
sendAndExpectFrameCount(90)
});
Okay, so the problem is that the first test passes, but the second test says the spy hasn't had any calls.
I think Cypress resets the spy by default in between tests, but I also re-set the spy in my beforeEach.
The spy sees the calls in the first test but not in the second test. The browser doesn't reload in between tests, so the spy is definitely using the same canvas context every time.
What is wrong here!?

Related

Is having more than one API fetch request in a React useEffect hook always very slow?

I am writing a simple application using React to fetch and display data from the Star Wars API. I first fetch information about a particular planet. The response JSON for a given planet contains a bunch of data, including an array of URLs pointing to further data about notable residents of said planet. I next call each of those URLs in order to display a list of the names of the residents of the current planet.
This code works, but is slow as heck:
const url = `https://swapi.dev/api/planets/`;
const [currentPlanetNumber, setCurrentPlanetNumber] = React.useState(1);
const [currentPlanet, setCurrentPlanet] = React.useState({});
const [currentPlanetResidentsDetails, setCurrentPlanetResidentsDetails] =
React.useState([]);
React.useEffect(() => {
(async () => {
const planetData = await fetch(`${url}${currentPlanetNumber}/`).then(
(response) => response.json()
);
setCurrentPlanet(planetData);
if (planetData.residents.length === 0) {
setCurrentPlanetResidentsDetails(["No notable residents"]);
} else {
const residentsURLs = planetData.residents;
const residentsNames = await Promise.all(
residentsURLs.map(async (item) => {
const name = await fetch(item).then((response) => response.json());
const newName = name.name;
return newName;
})
);
setCurrentPlanetResidentsDetails(residentsNames);
}
})();
}, [currentPlanetNumber]);
The following code works fairly fast for this:
const url = `https://swapi.dev/api/planets/`;
const [currentPlanetNumber, setCurrentPlanetNumber] = React.useState(1);
const [currentPlanet, setCurrentPlanet] = React.useState({});
const [currentPlanetResidentsDetails, setCurrentPlanetResidentsDetails] =
React.useState([]);
React.useEffect(() => {
(async () => {
const planetData = await fetch(`${url}${currentPlanetNumber}/`).then(
(response) => response.json()
);
setCurrentPlanet(planetData);
})();
}, [currentPlanetNumber]);
React.useEffect(() => {
(async () => {
if (currentPlanet.residents.length === 0) {
setCurrentPlanetResidentsDetails(["No notable residents"]);
} else {
const residentsURLs = currentPlanet.residents;
const residentsNames = await Promise.all(
residentsURLs.map(async (item) => {
const name = await fetch(item).then((response) => response.json());
const newName = name.name;
return newName;
})
);
setCurrentPlanetResidentsDetails(residentsNames);
}
})();
}, [currentPlanet]);
What makes the second one so much faster? I assumed that they would both take about the same length of time, because the same number of fetch requests get done either way.
Is it a good rule of thumb to not have more than one fetch request an any given useEffect hook?
No, there is no rule of thumb stating not to have more than one fetch request in a given useEffect.
In your first example, the fetch requests are fired consecutively, while in the second example they are fired concurrently.
Your first example seems to be more appropriate than the second. In the second example, it seems to you that the code is executing faster because both effects are firing concurrently when the component mounts. On subsequent changes to 'currentPlanetNumber', both examples should execute in the same amount of time.

Comparing results of function call before and after page actions

I have a complicated selection that I've put into a function to keep the test clean. I want to call the function before and after some page actions and compare the results.
This is my code, problem is I'm not getting the result back even though the value is extracted successfully inside the function.
const getVals = () => {
// simplified
cy.get('[id="22"] span')
.then($els => {
const vals = [...$els].map(el => el.innerText)
return vals
})
}
const vals1 = getVals()
// perform action on the page
const vals2 = getVals()
// compare
expect(vals1).to.deep.eq(vals2)
The function has a return inside .then() but it's not returning the result from the main body of the function.
Since the commands are asynchronous, I recommend changing to a custom command and adding alias to preserve the result during the intermediate actions
Cypress.Commands.add('getVals', () => {
cy.get('[id="22"] span')
.then($els => {
const vals = [...$els].map(el => el.innerText)
return vals // vals is Cypress "subject"
})
}
cy.getVals().as('vals1')
// perform action on the page
cy.getVals().then(vals2 => { // use then to obtain vals
cy.get('#vals1').then(vals1 => { // retrieve first from alias
// compare
expect(vals1).to.deep.eq(vals2)
})
})

Multiple subscriptions nested into one subscription

I find myself puzzled trying to set a very simple rxjs flow of subscriptions. Having multiple non-related subscriptions nested into another.
I'm in an angular application and I need a subject to be filled with next before doing other subscriptions.
Here would be the nested version of what I want to achieve.
subject0.subscribe(a => {
this.a = a;
subject1.subscribe(x => {
// Do some stuff that require this.a to exists
});
subject2.subscribe(y => {
// Do some stuff that require this.a to exists
});
});
I know that nested subscriptions are not good practice, I tried using flatMap or concatMap but didn't really get how to realize this.
It's always a good idea to separate the data streams per Observable so you can easily combine them later on.
const first$ = this.http.get('one').pipe(
shareReplay(1)
)
The shareReplay is used to make the Observable hot so it won't call http.get('one') per each subscription.
const second$ = this.first$.pipe(
flatMap(firstCallResult => this.http.post('second', firstCallResult))
);
const third$ = this.first$.pipe(
flatMap(firstCallResult => this.http.post('third', firstCallResult))
);
After this you can perform subscriptions to the Observables you need:
second$.subscribe(()=>{}) // in this case two requests will be sent - the first one (if there were no subscribes before) and the second one
third$.subscribe(() => {}) // only one request is sent - the first$ already has the response cached
If you do not want to store the first$'s value anywhere, simply transform this to:
this.http.get('one').pipe(
flatMap(firstCallResult => combineLatest([
this.http.post('two', firstCallResult),
this.http.post('three', firstCallResult)
])
).subscribe(([secondCallResult, thirdCallResult]) => {})
Also you can use BehaviorSubject that stores the value in it:
const behaviorSubject = new BehaviorSubject<string>(null); // using BehaviorSubject does not require you to subscribe to it (because it's a hot Observable)
const first$ = behaviorSubject.pipe(
filter(Boolean), // to avoid emitting null at the beginning
flatMap(subjectValue => this.http.get('one?' + subjectValue))
)
const second$ = first$.pipe(
flatMap(firstRes => this.http.post('two', firstRes))
)
const third$ = first$.pipe(
flatMap(()=>{...})
)
behaviorSubject.next('1') // second$ and third$ will emit new values
behaviorSubject.next('2') // second$ and third$ will emit the updated values again
You can do that using the concat operator.
const first = of('first').pipe(tap((value) => { /* doSomething */ }));
const second = of('second').pipe(tap((value) => { /* doSomething */ }));
const third = of('third').pipe(tap((value) => { /* doSomething */ }));
concat(first, second, third).subscribe();
This way, everything is chained and executed in the same order as defined.
EDIT
const first = of('first').pipe(tap(value => {
// doSomething
combineLatest(second, third).subscribe();
}));
const second = of('second').pipe(tap(value => { /* doSomething */ }));
const third = of('third').pipe(tap(value => { /* doSomething */ }));
first.subscribe();
This way, second and third are running asynchronously as soon as first emits.
You could do something like this:
subject$: Subject<any> = new Subject();
this.subject$.pipe(
switchMap(() => subject0),
tap(a => {
this.a = a;
}),
switchMap(() => subject1),
tap(x => {
// Do some stuff that require this.a to exists
}),
switchMap(() => subject2),
tap(y => {
// Do some stuff that require this.a to exists
})
);
if you want to trigger this, simply call this.subject$.next();
EDIT:
Here is an possible approach with forkJoin, that shout call the subjects parallel.
subject$: Subject<any> = new Subject();
this.subject$.pipe(
switchMap(() => subject0),
tap(a => {
this.a = a;
}),
switchMap(
() => forkJoin(
subject1,
subject2
)),
tap([x,y] => {
// Do some stuff that require this.a to exists
})
);

RXJS: Subscription to share operator causes strange behaviour

Using these RxJS tools: BehaviorSubject, Subscribe and Next
Please refer to the this codesandbox, and look at the console to see a visual: https://codesandbox.io/s/fancy-bird-0m81p
You will notice the object "C" value in the subscription is "1 stream behind
Consider the the following code:
const initialState = { a: 1, b: 2, c: 3 };
const Store$ = new BehaviorSubject(initialState);
const StoreUpdates$ = Store$.pipe(
scan((acc, curr) => {
return Object.assign({}, acc, curr);
}, initialState),
share()
);
export const updateStore = update => {
Store$.next(update);
};
StoreUpdates$.pipe(
distinctUntilChanged((p, n) => {
return p.b === n.b;
})
).subscribe(store => {
Store$.next({ c: Math.random() });
});
StoreUpdates$.subscribe(store => {
console.log("Subscription Check:: Notice issue here", store);
});
When you call the updateStore function, in the console.log you will notice that the C value, which is updated in a next call within a subscription, appears in the first iteration and the older value appears in the last iteration. So somehow it looks the next.call within the subscription happens "Before"
I believe the codesandox will illustrate and make it more clear.
How do I maintain the correct order of events so that the latest update appears last in the subscription?
For the moment a non-ideal way of solving this issue is to wrap the next statement within the subscribe in a timeout. If anyone has a better solution please do post.
StoreUpdates$.pipe(
distinctUntilChanged((p, n) => {
return p.b === n.b;
})
).subscribe(store => {
setTimeout(() => {
Store$.next({ c: Math.random() });
},0)
});

How can I reset accumulator of scan on ReplaySubject?

I have a ReplaySubject that accumulate data with scan operator and every 10000 ms should be reset. Is there any another way to do it?
Now:
let subject = new ReplaySubject();
subject.scan((acc, cur) => {
acc.push(cur);
return acc;
}, [])
.subscribe(events => {
localStorage.setItem('data', JSON.stringify(events))
});
subject
.bufferTime(10000)
.map(() => {
subject.observers[0]._seed = [];
})
.subscribe(() => localStorage.removeItem('data'));
I asked a very similar question few days ago and later answered myself
accumulating values such as with scan but with the possibility to reset the accumulator over time
maybe this can help you
SOME MORE DETAILS
An alternative approach is to have an Observable which acts as a timer which emits at a fixed interval, 10000ms in your case.
Once this timer emits, you pass the control to the Observable that cumululates via scan operator. To pass the control you use the switchMap operator to make sure the previous instance of the Observable completes.
If I understand correctly what you want to achieve, I would use a normal Subject rather than ReplaySubject.
The code could look something like this
const subject = new Subject<number>();
const timer = Observable.timer(0, 1000).take(4);
const obs = timer.switchMap(
() => {
console.log('-----');
return subject
.scan((acc, cur) => {
acc.push(cur);
return acc;
}, []);
}
)
obs.subscribe(
events => {
console.log(JSON.stringify(events))
}
);
// TEST DATA EMITTED BY THE SUBJECT
setTimeout(() => {
subject.next(1);
}, 100);
setTimeout(() => {
subject.next(2);
}, 1100);
setTimeout(() => {
subject.next(3);
}, 2100);
setTimeout(() => {
subject.next(4);
}, 2200);

Resources