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)
})
})
Related
I am writing a long test so I added the most reusable part to a Command Folder, however, I need access to a certain return value. How would I get the return value from the command?
Instead of directly returning the salesContractNumber, wrap it and then return it like this:
Your custom command:
Cypress.Commands.add('addStandardGrainSalesContract', () => {
//Rest of the code
return cy.wrap(salesContractNumber)
})
In your test you can do this:
cy.addStandardGrainSalesContract().then((salesContractNumber) => {
cy.get(FixingsAddPageSelectors.ContractNumberField).type(salesContractNumber)
})
Generally speaking, you need to return the value from the last .then().
Cypress puts the results of the commands onto the queue for you, and trailing .then() sections can modify the results.
Cypress.Commands.add('addStandardGrainSalesContract', () => {
let salesContractNumber;
cy.get('SalesContractsAddSelectors.SalesContractNumber').should($h2 => {
...
salesContractNumber = ...
})
.then(() => {
...
return salesContractNumber
})
})
cy.addStandardGrainSalesContract().then(salesContractumber => {
...
Or this should work also
Cypress.Commands.add('addStandardGrainSalesContract', () => {
cy.get('SalesContractsAddSelectors.SalesContractNumber').should($h2 => {
...
const salesContractNumber = ...
return salesContractNumber; // pass into .then()
})
.then(salesContractNumber => {
...
return salesContractNumber // returns to outer code
})
})
cy.addStandardGrainSalesContract().then(salesContractumber => {
...
Extra notes:
const salesContractHeader = $h2.text() // don't need Cypress.$()
const salesContractNumber = salesContractHeader.split(' ').pop() // take last item in array
We are using .pipe(takeUntil) in the logincomponent.ts. What I need is, it should get destroyed after successful log in and the user is on the landing page. However, the below snippet is being called even when the user is trying to do other activity and hitting submit on the landing page should load different page but the result of submit button is being overridden and taken back to the landing page.
enter code hereforkJoin({
flag: this.auth
.getEnvironmentSettings('featureEnableQubeScan')
.pipe(take(1)),
prefs: this.auth.preferences.pipe(take(1)),
}).subscribe(
(result: any) => {
this.qubeScanEnabled = result.flag.featureEnableQubeScan;
this.userPrefs = result.prefs;
// check to see if we're authed (but don't keep listening)
this.auth.authed
.pipe(takeUntilComponentDestroyed(this))
.subscribe((payload: IJwtPayload) => {
if (payload) {
this.auth.accountO
.pipe(takeUntilComponentDestroyed(this))
.subscribe((account: IAccount) => {
if (this.returnUrl) {
this.router.navigateByUrl(this.returnUrl);
} else {
this.router.navigate(['dashboard']);
}
}
}
}
}
);
ngOnDestroy() {}
Custom Code:
export function takeUntilComponentDestroyed(component: OnDestroy) {
const componentDestroyed = (comp: OnDestroy) => {
const oldNgOnDestroy = comp.ngOnDestroy;
const destroyed$ = new ReplaySubject<void>(1);
comp.ngOnDestroy = () => {
oldNgOnDestroy.apply(comp);
destroyed$.next(undefined);
destroyed$.complete();
};
return destroyed$;
};
return pipe(
takeUntil(componentDestroyed(component))
);
}
Please let me know what I am doing wrong.
Versions:
rxjs: 6.5.5
Angular:10.0.8
Thanks
I've done a first pass at creating a stream that doesn't nest subscriptions and continues to have the same semantics. The major difference is that I can move takeUntilComponentDestroyed to the end of the stream and lets the unsubscibes filter backup the chain. (It's a bit cleaner and you don't run the same code twice every time through)
It's a matter of taste, but flattening operators are a bit easier to follow for many.
enter code hereforkJoin({
flag: this.auth
.getEnvironmentSettings('featureEnableQubeScan')
.pipe(take(1)),
prefs: this.auth.preferences.pipe(take(1)),
}).pipe(
tap((result: any) => {
this.qubeScanEnabled = result.flag.featureEnableQubeScan;
this.userPrefs = result.prefs;
}),
mergeMap((result: any) => this.auth.authed),
filter((payload: IJwtPayload) => payload != null),
mergeMap((payload: IJwtPayload) => this.auth.accountO),
takeUntilComponentDestroyed(this)
).subscribe((account: IAccount) => {
if (this.returnUrl) {
this.router.navigateByUrl(this.returnUrl);
} else {
this.router.navigate(['dashboard']);
}
});
This function doesn't create another inner stream (destroyed$). This way is a bit more back to the basics so it should be easier to debug if you're not getting the result you want.
export function takeUntilComponentDestroyed<T>(comp: OnDestroy): MonoTypeOperatorFunction<T> {
return input$ => new Observable(observer => {
const sub = input$.subscribe({
next: val => observer.next(val),
complete: () => observer.complete(),
error: err => observer.error(err)
});
const oldNgOnDestroy = comp.ngOnDestroy;
comp.ngOnDestroy = () => {
oldNgOnDestroy.apply(comp);
sub.unsubscribe();
observer.complete();
};
return { unsubscribe: () => sub.unsubscribe() };
});
}
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!?
I have a simple saga of this form:
const getAccountDetails = function * () {
const { url } = yield select(state => state.appConfig)
const accountDetails = yield call(apiFetchAccountDetails, url)
}
I'm trying to write a unit test:
describe('getAccountDetails', () => {
const iterator = getAccountDetails()
it("should yield an Effect 'select(state=> state.appConfig)'", () => {
const effect = iterator.next().value
const expected = select(state => state.appConfig)
expect(effect).to.deep.eql(expected)
})
This test fails. Although effect and expected are very similar, they are not identical.
At least one of the differences is buried in payload.selector.scopes, where the yielded effect and expected are as follows:
As the scopes of these two will always be different, how can these tests ever be made to work?
eta: this pattern is adapted from the example linked to from the redux-saga docs
Cracked it after finding this issue from way back.
The fix is to create a named function to do the select and export it from the module where the saga under test lives, and then use this same function in tests. All is well.
export const selectAppConfig = state => state.appConfig
const getAccountDetails = function * () {
const { url } = yield select(selectAppConfig)
const accountDetails = yield call(apiFetchAccountDetails, url)
}
import {selectAppConfig} from './sagaToTest'
describe('getAccountDetails', () => {
const iterator = getAccountDetails()
it("should yield an Effect 'select(state=> state.appConfig)'", () => {
const effect = iterator.next().value
const expected = select(selectAppConfig)
expect(effect).to.deep.eql(expected)
})
I was trying to return filter function but return doesn't seem to work with callbacks. Here this.store.let(getIsPersonalized$) is an observable emitting boolean values and this.store.let(getPlayerSearchResults$) is an observable emiting objects of video class.
How do I run this synchronously, can I avoid asynchronus callback altogether given that I can't modify the observables received from store.
isPersonalized$ = this.store.let(getIsPersonalized$);
videos$ = this.store.let(getPlayerSearchResults$)
.map((vids) => this.myFilter(vids));
myFilter(vids) {
this.isPersonalized$.subscribe((x){
if(x){
return this.fileterX(vids);//Return from here
}
else {
return this.filterY(vids);//Or Return from here
}
});
}
fileterX(vids) {
return vids.filter((vid) => vids.views>100;);
}
fileterY(vids) {
return vids.filter((vid) => vids.views<20;);
}
I got it working this way, you don't need myFilter(vids) at all if you can get the branching out on isPersonalized$'s subscribe. Here is the updated code.
this.store.let(getIsPersonalized$);
videos$: Observable<any>;
ngOnInit() {
this.isPersonalized$.subscribe((x) => {
if (x) {
this.videos$ = this.store.let(getPlayerSearchResults$)
.map((vids) => this. fileterX(vids));
} else {
this.videos$ = this.store.let(getPlayerSearchResults$)
.map((vids) => this. fileterY(vids));
}
});
}
fileterX(vids) {
return vids.filter((vid) => vids.views>100;);
}
fileterY(vids) {
return vids.filter((vid) => vids.views<20;);
}
It looks like you want to evaluate the latest value of isPersonalized$ within the map function, i'd do that via withLatestFrom (Example: The first one toggles true/false every 5s, the second emits an increasing number every 1s):
const isPersonalized$ = Rx.Observable.interval(5000)
.map(value => value % 2 === 0);
const getPlayerSearchResults$ = Rx.Observable.interval(1000)
.withLatestFrom(isPersonalized$)
.map(bothValues => {
const searchResult = bothValues[0];
const isPersonalized = bothValues[1];
...
});