Observable finally not getting fired - rxjs

This works:
this.http.get('/doesntexist1')
.finally(() => console.log('finally1'))
.subscribe(() => { });
But this doesn't:
const obs = this.http.get('/doesntexist2');
obs.finally(() => console.log('finally2'))
obs.subscribe(() => { });
Both URL produce a 404.
I run both and I only see "finally1" being displayed in the console, any idea why?

The difference is that in the second example the .finally is not in the same stream as the .subscribe since you aren't chaining them.
Your first example creates this stream:
get -> finally -> subscribe
Your second example creates two branches:
get -> finally
get -> subscribe
The finally wont be executed without a subscription after it.
If you want to build a stream then you need to chain the operators by using the result of the previous operator. It isn't like an in-place operator.

Related

Cypress: wrap chained commands in one

I use this long line to check the selected value on any list on my page (using Ember power-select which is not a but a complex set of ), selector being the parent so I can target the list I want, and trimming being there so I can chain a .should('eq', expected_value)
cy.get(selector).find('span[class="ember-power-select-selected-item"]').invoke("text").then((text) => text.trim())
I would love to shorten all the commands after get in one, and be able to call something like
cy.get(selector).selected_value()
I've started reading on custom commands, wrap, invoke... but am too new on Cypress to understand the right way to do this.
To add a custom command, you may add the following inside cypress/support/commands.js file:
Cypress.Commands.add('selected_value', { prevSubject: true}, (subject) => {
return cy.wrap(subject).find('span[class="ember-power-select-selected-item"]').invoke("text").then((text) => text.trim())
})
Note that we use the prevSubject option to be able to chain our command from an initial cy.get(selector) one.
And now you are able to use the command:
cy.get(selector).selected_value().should('eq', expected_value)
Also, it's better to provide a type script definition file for the new command so IDE can be aware of it and provide the autocomplete and other features.
Adding a command might look a bit complicated for a beginner or annoying for an experienced user. There is Cypress Pro plugin (I'm the author) for the IntelliJ platform that can simplify creating and maintaining custom commands.
Creating a custom command can make your test more readable, encapsulate a group of commands that are repeated and need to be parameterized.
For your example testing the text of dropdown items, you can pass in the text and return the select so that chaining is possible
NOTES
In your code, .find().invoke("text") returns all the item text in one string, so I added .contains() to select an individual item.
If you're only interested in a partial match, the command chain can stop at .contain(text.trim())
Cypress.Commands.add('hasSelectedItemText',
{ prevSubject: true },
(subject, text) => {
cy.wrap(subject)
.find('span[class="ember-power-select-selected-item"]')
.contains(text.trim())
.invoke("text")
.should('eq', text.trim())
cy.wrap(subject) // returns the original select
}
)
cy.get(selector)
.hasSelectedItemText('one')
.hasSelectedItemText('two')
.hasSelectedItemText('three')
A more complicated example using the dual type command. Here the command can be parent or child, so the parameters have different meanings depending on usage
Cypress.Commands.add("dropdownItemText",
{ prevSubject: "optional" },
(subject, arg1, arg2) => {
if (subject) {
const text = arg1
cy.wrap(subject)
.find('span[class="ember-power-select-selected-item"]')
.contains(text.trim())
.invoke("text")
.should('eq', text.trim())
cy.wrap(subject) // returns the original select
} else {
const text = arg2
cy.get(arg1)
.find('span[class="ember-power-select-selected-item"]')
.contains(text.trim())
.invoke("text")
.should('eq', text.trim())
cy.get(arg1) // make select the returned "subject" for further chaining
}
}
)
cy.dropdownItemText(selector, 'one')
.dropdownItemText('two')
.dropdownItemText('three')

can we use cypress wait without then block?

https://docs.cypress.io/api/commands/wait - cy.wait() is used to wait for an arbitrary number of milliseconds.
What's the difference in the following two snippets
// snippet-1
cy.wait(10*1000);
// other code
// snippet - 2
cy.wait(10*1000).then(()=>{
// other code
})
I tried to run the following code to test how it is working and i got the output mentioned in the below image.
cy.wait(10 * 1000).then(() =>
cy.log("This is inside wait()" + new Date().toLocaleTimeString())
);
cy.log("This is after wait()" + new Date().toLocaleTimeString());
These are the log results:
From the above image, it looks like we should add our code inside .then because that will be executed after cy.wait . I also have seen people writing their test cases just like snippet-1
Which one is correct and which one we should use?
If you want a better log, use a custom command to make a version that defers evaluating the value.
By providing a callback, the new Date().toLocaleTimeString() expression evaluates when you expect it to.
Cypress.Commands.add('logLazy', (thingToLog) => {
if (typeof thingToLog === 'function') {
cy.log(thingToLog())
} else {
cy.log(thingToLog)
}
})
cy.wait(3_000).then(() =>
cy.log("This is inside wait()" + new Date().toLocaleTimeString())
);
cy.log("This is after wait()" + new Date().toLocaleTimeString());
cy.wait(3_000).then(() =>
cy.logLazy("This is LAZY LOG inside wait()" + new Date().toLocaleTimeString())
);
cy.logLazy(() => "This is LAZY LOG after wait()" + new Date().toLocaleTimeString());
Cypress commands use the current value of variables and resolve function values when enqueuing commands. So, in your example, the commands are enqueued as .wait() and then the outer .log(). The value of the date string is calculated when the command is enqueued. The inner .log() is enqueued within the .then(), and thus has a later date string.
Either snippet is fine to use, but be aware of how the subsequent commands in your test will react. Are you manipulating some variable during your .wait(), and you would not want the original value to be used for your Cypress command? Probably a good idea to place that command within a .then() or some other Cypress command. Are you just interacting with the website and need to wait 10 seconds? You probably don't need to worry about placing commands inside .then().

Wrong Cypress intercept being hit

I have set up a number of intercepts in the body of my tests. Here they are, pasted from the Cypress log, with the alias added
cy:intercept ➟ // alias: getRecipesSku(971520)
Method: GET
Matcher: "https://wpsite.com/wp-json/wp/v2/posts?tags[]=6287&_fields**"
Mocked Response: [{ ... }]
cy:intercept ➟ // alias: getRecipesSku(971520,971310)
Method: GET
Matcher: "https://wpsite.com/wp-json/wp/v2/posts?tags[]=6287&tags[]=6289&_fields**"
Mocked Response: [{ ... }]
Our application's tests also mocks a number of routes by default, (coming from an apiClient.initialize) including this one below. FWIW, this is defined earlier than those above:
cy:intercept ➟ // alias: getJustcookRecipes
Method: GET
Matcher: "https://wpsite.com/wp-json/**"
Mocked Response: [{ ... }]
My test code sets up those first two intercepts, and ultimately calls the routes. Here is the code, heavily abridged:
it('refreshes the recipes when switching protein tabs', () => {
apiClient.initialize()
/* do lots of other stuff; load up the page, perform other tests, etc */
// call a function that sets up the intercepts. you can see from the cypress output
// that the intercepts are created correctly, so I don't feel I need to include the code here.
interceptJustCook({ skus: [beefCuts[0].id] }, [beefCut1Recipe])
interceptJustCook({ skus: [beefCuts[0].id, beefCuts[1].id] }, twoBeefRecipes)
// [#1] select 1 item;
// calls route associated with intercept "getRecipesSku(971520)"
page.click.checkboxWithSku(beefCuts[0].id)
/* assert against that call */
// [#2] select 2nd item (now 2 items are selected);
// calls route associated with intercept "getRecipesSku(971520, 971310)"
page.click.checkboxWithSku(beefCuts[1].id)
In the Cypress logs, the first call (marked by comment #1) is intercepted correctly:
cy:fetch ➟ (getRecipesSku(971520)) STUBBED
GET https://wpsite.com/wp-json/wp/v2/posts?tags[]=6287&_fields=jetpack_featured_media_url,title.rendered,excerpt.rendered,link,id&per_page=100&page=1&orderby=date
However, the second call (marked by comment #2) is intercepted by the wrong route mocker:
cy:fetch ➟ (getJustCookRecipes) STUBBED
GET https://wpsite.com/wp-json/wp/v2/posts?tags[]=6287&tags[]=6289&_fields=jetpack_featured_media_url,title.rendered,excerpt.rendered,link,id&per_page=100&page=1&orderby=date
You can see for yourself that the URL called at #2 does indeed match the getRecipesSku(971520, 971310) intercept, but it is caught by the getJustcookRecipes intercept. Now, I suppose the URL for that latter intercept would catch my second custom intercept. But it would also, in the same way, catch my first custom intercept, but that first one works.
(update:) I tried commenting out the place in the code where the getJustcookRecipes intercept is created so that it doesn't exist. Now, the call that should hit getRecipesSku(971520,971310) isn't being mocked at all! I checked and the mocked and called urls are a match.
Why is this going wrong and how do I fix it?
Something in the glob pattern for the 2nd intercept #getRecipesSku(971520,971310) is refusing to match.
It's probably not worth while analyzing what exactly (you may not be able to fix it in glob), but switching to a regex will match.
See regex101.com online test
cy.intercept(/wpsite\.com\/wp-json\/wp\/v2\/posts\?tags\[]=6287&tags\[]=6289&_fields/, {})
.as('getRecipesSku(971520,971310)')
The request query string may be badly formed
Looking at the docs for URLSearchParams, the implication is that the query string should be key/value pairs.
But the 2nd request has two identical keys using tags[] as the key.
It looks as if the correct format would be /wp/v2/posts?tags=[6287,6289] since the square brackets don't have a lot of meaning otherwise.
It may be that the server is handling the format tags[]=6287&tags[]=6289, but Cypress is probably not. If you run the following intercept you see the query object has only one tags[] key and it's the last one in the URL.
cy.intercept({
method: 'GET',
pathname: '/wp-json/wp/v2/posts',
},
(req) => {
console.log('url', req.url)
console.log('query', req.query) // Proxy {tags[]: '6289', ...
}
)
#Paolo was definitely on the right track. The WP API we're consuming uses tags[]=1,tags=[2] instead of tags[1,2] so that's correct, but some research into globs showed me that brackets are special. Why the first bracket isn't messing it up but the second one is, I'm not sure and I kind of don't care.
I thought about switching to regex, but instead I was able to escape the offender in the glob without switching to regex and needing to escape aaaallll the slashes in the url:
const interceptRecipes = (
{ category, skus }: RecipeFilterArgs,
recipes: Recipe[]
) => {
let queryString = ''
if (category) queryString = `categories[]=${CATEGORY_MAP[category]}`
if (skus) {
const tagIdMap = SkuToTagIdMap as Record<number, { tagId: number }>
// brackets break the glob pattern, so we need to escape them
queryString = skus.map((sku) => `tags\\[]=${tagIdMap[sku].tagId}`).join('&')
}
const url = `${baseUrl}?${queryString}${otherParams}`
const alias = category ? `get${category}Recipes` : `getRecipesSku(${skus})`
cy.intercept('GET', url, recipes).as(alias)
}

Can I update the deps array in useEffect?

I am using Effect Hook and want to confirm if this implementation is correct. I am not getting any warning in the console but I want to know why this is not going into an infinite loop?
React.useEffect(() => {
setSelections(inputUPC, false);
//console.log(props.uncheckCard);
props.setUncheckCard(false);
}, [props.uncheckCard]);
It's not going into infinite loop because you only set this to false so there is no change in parameter (useEffect only trigger on dependencies values change)

RXJS sequential HTTP request

I would like to do sequential HTTP calls and merge the responses into one and unique observable.
Problem is: the way I am doing it seems to be wrong. Indeed, I would like to merge the data from the second request into the first one, but the way I did it seems to replace the result of the first one by the result of the seconde one.
Here it is the first request called:
[{vehicule_id:123, statistics:{speed:60,rotation:38}},...]
The second one:
[{vehicule_id:123, name:"name",description:"description},...]
And the result I would like to get:
[{vehicule_id:123, statistics:{speed:60,rotation:38}, name:"name",description:"description},...]
Important thing to know: the second request needs a vehicule_id provided in the response of the first one.
With my code, the response of the second call replace the result of the first one instead of merging them.
Here it is my code:
getUserVehiculesInfo(user_id: number, language: string): void {
this._data.getUserVehiculesInfo(user_id, language)
.pipe(
map(res => res.data[user_id]),
switchMap(vehicules => forkJoin(vehicules.map(vehicule => this._data.getVehiculesInfo(tank.vehicule_id, language)))),
tap(console.log)
)
.subscribe();
}
Once you have the vehiculesInfo (the result of the forkJoin, you simply need to combine them with the vehicules:
switchMap(
vehicules => forkJoin(...).pipe(
map(infos => combineVehiculesAndInfos(vehicules, infos))
)
)

Resources