I am using react-apollo to make GraphQL queries, and I am using Cypress for testing.
The problem is that these 2 dont seem to play well along. Apollo seems to be making all its requests through the Fetch API.
But Cypress seems like it is not able to capture anything, except XHR requests.
So what could I do to solve this problem? Is there a way for Cypress to capture "fetch" requests? Is there a way for react-apollo to use "xhr" instead of "fetch"?
Try out cypress-graphql-mock
You just pass it your schema:
const schema = fs.readFileSync('../../app-schema.graphql', 'utf8');
// alternatively, using a dumped introspection query:
// const schema = require('../../dumped-schema.json')
beforeEach(() => {
cy.server();
cy.mockGraphql({ schema });
});
An easy workaround is to use whatwg-fetch, which you'd add as a dependency via npm and then...
cypress/support/fetch_to_xhr.js
function fetchToXhr() {
let polyfill
before(() => {
cy.readFile('node_modules/whatwg-fetch/dist/fetch.umd.js')
.then((contents) => polyfill = contents)
Cypress.on('window:before:load', (win) => {
delete win.fetch
win.eval(polyfill)
})
})
}
fetchToXhr()
cypress/support/index.js
import "./fetch_to_xhr";
After that cypress will capture the graphql requests
Cypress now officially supports working with GQL: Working with GraphQL.
I recommend reading their documentation in detail, but at your own risk here is a VERY crude TLDR:
cy.intercept('your-url-here/graphql', (req) => {
const { body } = req;
if (body.hasOwnProperty('your-operationName-here')) {
// do something with the request, like:
req.reply('your-mock-here');
}
});
Related
I'm using nuxt.js and now need to generate my dynamic pages by running npm run generate. However my list items needed to make dynamic items are stored in the store, so I need to map over them somehow so the generate can make the routes for them
How can I access the store in my nuxt.config.js?
generate: {
dir: 'wwwroot', //override the default generation dir to create everything straight in wwwroot
routes() {
let vdc = this.$store.vdcServers.map(server => `/virtual-data-centres/${server.slug}`);
return Promise.all([vdc]).then(values => {
return values.join().split(',');
})
}
}
Output
ERROR Could not resolve routes
FATAL Cannot read properties of undefined (reading '$store')
To my knowledge, you cannot access the store from that place. Maybe with some hooks? I doubt.
Meanwhile, if you have your elements available in your store you should be able to find them back by making a quick axios call or like I think.
This kind of approach is totally fine
import axios from 'axios'
export default {
generate: {
routes(callback) {
axios
.get('https://my-api/users')
.then(res => {
const routes = res.data.map(user => {
return '/users/' + user.id
})
callback(null, routes)
})
.catch(callback)
}
}
}
It may be a bit of code duplication, meanwhile, it's still the simplest way to go.
Otherwise, you could try to persist it to some localStorage or any similar solution, to have it both in Vuex and during your generation.
Problem Solved
If you're struggling with the same issue, look at the accepted answer which is one way to achieve it by using serverMiddleware
I'm using an API which required a private key. I've stored the key inside a .env file, and called it in the nuxt configuration file, like this :
privateRuntimeConfig: {
secretKey: process.env.MY_SECRET_KEY
},
My API call is done inside the asyncData() hook on my index page. It works fine when i load this page, or reload it, but everytime i use the navigation to come back to this page, i end up with an error (I use a buffer to convert my API key to base64)
First argument must be a string, Buffer, ArrayBuffer, Array, or array-like object.
After some research and debugging, i found out that my private key wasn't available at the time, and the "secret" value used in my api call was "undefined".
The thing I don't get is why is this working on initial load / reload but not on page navigation ? And is there a way to fix it without using a backend ? (SSR for SEO and the ability to use private keys without exposing them are the main reasons why i used Nuxt for my project)
Here is my code :
async asyncData({ $content, store, $config }) {
const secret = Buffer.from($config.secretKey).toString('base64')
const request = await fetch('https://app.snipcart.com/api/products', {
headers: {
'Authorization': `Basic ${secret}`,
'Accept': 'application/json'
}
})
const result = await request.json()
store.commit('products/addProducts', result)
const stocks = store.getters['products/getProducts']
return { stocks }
},
Update
Looking at the #nuxtjs/snipcart module's key key and since it's a buildModules, you can totally put it there since it will be available only during the build (on Node.js only)!
For more info, Snipcart do have a lot of blog posts, this one based on Nuxt may help clearing things up: https://www.storyblok.com/tp/how-to-build-a-shop-with-nuxt-storyblok-and-snipcart
You do have your key initially because you're reaching the server when you enter the page or hard refresh it.
If you navigate after the hydration, it will be a client side navigation so you will not be able to have access to the private key. At the end, if your key is really private (nowadays, some API provide keys that can be exposed), you'll need to work around it in some ways.
Looking at Snipcart: https://docs.snipcart.com/v3/api-reference/authentication, it clearly states that the key should be available in
Appear in your compiled front-end assets (HTML, JavaScript)
Meanwhile, if you need to make another call to your backend (trying to access something else than products), you'll need to make a second call.
With Nuxt2, you cannot reach for the backend each time as of right now since you will stay in an SPA context (Nuxt is a server then client Vue app basically). But you could write down the token into a cookie or even better, use a backend as a proxy to hide this specific key (or even a serverless function).
Some more info can be found on my other answer here: https://stackoverflow.com/a/69575243/8816585
Thanks #kissu for your (very) quick answer :)
So, based on what you said and your other answer on the subject, i've made a server Middleware in Nuxt in my server folder.
server/snipcart.js
const bodyParser = require('body-parser')
const axios = require('axios')
const app = require('express')()
app.use(bodyParser.json())
app.all('/getProducts', (request, response) => {
const url = 'https://app.snipcart.com/api/products'
const secret = Buffer.from(process.env.SNIPCART_SECRET).toString('base64')
const config = {
headers: {
'Authorization': `Basic ${secret}`,
'Accept': 'application/json'
}
}
axios
.get(url, config)
.then(res => {
const products = {}
res.data.items.forEach(
item => {
const productId = item.userDefinedId.replace(/-/g, '')
const stocks = {}
item.variants.forEach(
variant => {
const size = variant.variation[0].option
const stock = variant.stock
stocks[size] = stock
}
)
products[productId] = stocks
}
)
response.json(products)
})
.catch( err => response.json(err) )
})
module.exports = app
Correct me if i'm wrong, but I think that's basically the same as using a server as a proxy right ? Based on Nuxt lifecycle hooks, the serverMiddleware one is only run on the server, so my API key shouldn't be exposed to the client ? (I still need to do some refactoring to clean the code, but at least it's working) (https://nuxtjs.org/docs/concepts/nuxt-lifecycle/#server & https://nuxtjs.org/docs/configuration-glossary/configuration-servermiddleware/)
nuxt.config.js
serverMiddleware: [
{ path: "/server", handler: "~/server/snipcart.js" }
]
index.vue (where my snipcart API call was previously made, i guess now I should move this call directly from the product card component where the data is needed) :
async asyncData({ $content, store, $axios }) {
await $axios
.get('/server/getProducts')
.then(res => store.commit('products/addProducts', res.data))
.catch(err => console.log(err))
const stocks = store.getters['products/getProducts']
return {stocks, masterplanProducts }
},
PS : Snipcart does provide a public API key, but the use is very limited. In order to access the remaining stock for each product, i have to use the private key (which allows for some other operations, like removing products / accessing orders and such)
UPDATE :
It's not working when the website is fists accessed from any other page than the one one where the API call is, since the store won't have any data from the API call)
Okay, now I feel dumb. I found a way to make it work. I guess taking the time to explain my problem helped me understand how to solve it.
For those who encounter a similar issue, i fixed it by wrapping my API call with a If statement.
if ($config.secretKey) {
const secret = Buffer.from($config.secretKey).toString('base64')
const request = await fetch('https://app.snipcart.com/api/products', {
headers: {
'Authorization': `Basic ${secret}`,
'Accept': 'application/json'
}
})
const result = await request.json()
store.commit('products/addProducts', result)
}
const stocks = store.getters['products/getProducts']
This way, i can just skip the API call and access values from my vuex store.
I want to save/persist/preserve a cookie or localStorage token that is set by a cy.request(), so that I don't have to use a custom command to login on every test. This should work for tokens like jwt (json web tokens) that are stored in the client's localStorage.
To update this thread, there is already a better solution available for preserving cookies (by #bkucera); but now there is a workaround available now to save and restore local storage between the tests (in case needed). I recently faced this issue; and found this solution working.
This solution is by using helper commands and consuming them inside the tests,
Inside - cypress/support/<some_command>.js
let LOCAL_STORAGE_MEMORY = {};
Cypress.Commands.add("saveLocalStorage", () => {
Object.keys(localStorage).forEach(key => {
LOCAL_STORAGE_MEMORY[key] = localStorage[key];
});
});
Cypress.Commands.add("restoreLocalStorage", () => {
Object.keys(LOCAL_STORAGE_MEMORY).forEach(key => {
localStorage.setItem(key, LOCAL_STORAGE_MEMORY[key]);
});
});
Then in test,
beforeEach(() => {
cy.restoreLocalStorage();
});
afterEach(() => {
cy.saveLocalStorage();
});
Reference: https://github.com/cypress-io/cypress/issues/461#issuecomment-392070888
From the Cypress docs
For persisting cookies: By default, Cypress automatically clears all cookies before each test to prevent state from building up.
You can configure specific cookies to be preserved across tests using the Cypress.Cookies api:
// now any cookie with the name 'session_id' will
// not be cleared before each test runs
Cypress.Cookies.defaults({
preserve: "session_id"
})
NOTE: Before Cypress v5.0 the configuration key is "whitelist", not "preserve".
For persisting localStorage: It's not built in ATM, but you can achieve it manually right now because the method thats clear local storage is publicly exposed as Cypress.LocalStorage.clear.
You can backup this method and override it based on the keys sent in.
const clear = Cypress.LocalStorage.clear
Cypress.LocalStorage.clear = function (keys, ls, rs) {
// do something with the keys here
if (keys) {
return clear.apply(this, arguments)
}
}
You can add your own login command to Cypress, and use the cypress-localstorage-commands package to persist localStorage between tests.
In support/commands:
import "cypress-localstorage-commands";
Cypress.Commands.add('loginAs', (UserEmail, UserPwd) => {
cy.request({
method: 'POST',
url: "/loginWithToken",
body: {
user: {
email: UserEmail,
password: UserPwd,
}
}
})
.its('body')
.then((body) => {
cy.setLocalStorage("accessToken", body.accessToken);
cy.setLocalStorage("refreshToken", body.refreshToken);
});
});
Inside your tests:
describe("when user FOO is logged in", ()=> {
before(() => {
cy.loginAs("foo#foo.com", "fooPassword");
cy.saveLocalStorage();
});
beforeEach(() => {
cy.visit("/your-private-page");
cy.restoreLocalStorage();
});
it('should exist accessToken in localStorage', () => {
cy.getLocalStorage("accessToken").should("exist");
});
it('should exist refreshToken in localStorage', () => {
cy.getLocalStorage("refreshToken").should("exist");
});
});
Here is the solution that worked for me:
Cypress.LocalStorage.clear = function (keys, ls, rs) {
return;
before(() => {
LocalStorage.clear();
Login();
})
Control of cookie clearing is supported by Cypress: https://docs.cypress.io/api/cypress-api/cookies.html
I'm not sure about local storage, but for cookies, I ended up doing the following to store all cookies between tests once.
beforeEach(function () {
cy.getCookies().then(cookies => {
const namesOfCookies = cookies.map(c => c.name)
Cypress.Cookies.preserveOnce(...namesOfCookies)
})
})
According to the documentation, Cypress.Cookies.defaults will maintain the changes for every test run after that. In my opinion, this is not ideal as this increases test suite coupling.
I added a more robust response in this Cypress issue: https://github.com/cypress-io/cypress/issues/959#issuecomment-828077512
I know this is an old question but wanted to share my solution either way in case someone needs it.
For keeping a google token cookie, there is a library called
cypress-social-login. It seems to have other OAuth providers as a milestone.
It's recommended by the cypress team and can be found on the cypress plugin page.
https://github.com/lirantal/cypress-social-logins
This Cypress library makes it possible to perform third-party logins
(think oauth) for services such as GitHub, Google or Facebook.
It does so by delegating the login process to a puppeteer flow that
performs the login and returns the cookies for the application under
test so they can be set by the calling Cypress flow for the duration
of the test.
I can see suggestions to use whitelist. But it does not seem to work during cypress run.
Tried below methods in before() and beforeEach() respectively:
Cypress.Cookies.defaults({
whitelist: "token"
})
and
Cypress.Cookies.preserveOnce('token');
But none seemed to work. But either method working fine while cypress open i.e. GUI mode. Any ideas where I am coming short?
2023 Updated on Cypress v12 or more:
Since Cypress Version 12 you can use the new cy.session()
it cache and restore cookies, localStorage, and sessionStorage (i.e. session data) in order to recreate a consistent browser context between tests.
Here's how to use it
// Caching session when logging in via page visit
cy.session(name, () => {
cy.visit('/login')
cy.get('[data-test=name]').type(name)
cy.get('[data-test=password]').type('s3cr3t')
cy.get('form').contains('Log In').click()
cy.url().should('contain', '/login-successful')
})
Im looking to use $auth inside my Nuxt project, specially inside an axios plugin.
Here is my code:
plugins/api.js
export default function ({ $axios }, inject) {
const api = $axios.create({
headers: {
common: {
Accept: 'text/plain, */*',
},
},
})
// Set baseURL to something different
api.setBaseURL('http://localhost:4100/')
// Inject to context as $api
inject('api', api)
}
Now the problem comes when I try to use $auth from #nuxtjs/auth-next package.
As stated in the docs:
This module globally injects $auth instance, meaning that you can
access it anywhere using this.$auth. For plugins, asyncData, fetch,
nuxtServerInit and Middleware, you can access it from context.$auth.
I tried the following:
This results in $auth being undefined
export default function ({ $axios, $auth }, inject) {
This one was near
export default function ({ $axios, app }, inject) {
console.log(app) //This one logs $auth in the object logged
console.log(app.$auth) // I don't understand why but this one returns undefined
My main goal here is to make use of this.$auth.strategy.token.get()and pass it (if the token exists of course) to the headers of every request made using this.$api
I have been looking for similar questions and answers but none has helped me to solve this, I could just add the token every time I write this.$api but that would increase the code unnecessarily.
Thanks in advance to all the people for your time and help.
EDIT:
Okay, now I made a test. and the next code is actually logging the $auth object correctly, it seems some time is needed to make it work but now Im afraid that using setTimeout could cause an error because I can't know exactly how much time is needed for $auth to be available.
export default function ({ $axios, app }, inject) {
setTimeout(() => {
console.log('After timeout', app.$auth)
}, 50)
EDIT 2:
So now I have made more tests, and using 0 milliseconds instead of 50 works too, so I will use setTimeout with 0 milliseconds for now, I hope anyone find a better solution or explain why $auth is not available before using setTimeout so I can decide what to do with my code.
EDIT 3:
After trying to wrap all my previous code inside setTimeout I noticed that the code fails, so that isn't a solution.
I have found a solution so I will post it so that every person that could have the same problem in the future can solve it.
It turns out that I could easily solve it using interceptors.
export default function ({ $axios, app }, inject) {
// At this point app.$auth is undefined. (Unless you use setTimeout but that is not a solution)
//Create axios instance
const api = $axios.create({
headers: {
common: {
Accept: 'application/json', //accept json
},
},
})
// Here is the magic, onRequest is an interceptor, so every request made will go trough this, and then we try to access app.$auth inside it, it is defined
api.onRequest((config) => {
// Here we check if user is logged in
if (app.$auth.loggedIn) {
// If the user is logged in we can now get the token, we get something like `Bearer yourTokenJ9F0JFODJ` but we only need the string without the word **Bearer**, So we split the string using the space as a separator and we access the second position of the array **[1]**
const token = app.$auth.strategy.token.get().split(' ')[1]
api.setToken(token, 'Bearer') // Here we specify the token and now it works!!
}
})
// Set baseURL to something different
api.setBaseURL('http://localhost:4100/')
// Inject to context as $api
inject('api', api)
}
Also Nuxt Auth itself has provided a solution for this issue:
https://auth.nuxtjs.org/recipes/extend/
I am trying to retrieve the json response from api url using fetch (method : GET) in react-native.
constructor(props){
super(props);
this.state = {jsonData: {}};
}
componentWillMount() {
console.log("inside componentWillMount");
fetch('http://localhost:3000/listData')
.then((response) => {console.log('response: '); return response.json();})
.then((responseJson) => {console.log('responseData: '+JSON.stringify(responseJson)); return this.setState({jsonData: responseJson});})
.catch((err) => {console.log(err)});
}
The api returns data in this format:
{
object: 'list',
data: [...]
}
The api url works via curl. I also tried to run the fetch part of the code standalone using node by installing node-fetch and it printed the responseData properly.
But, in react-native, it doesn't print any of the console log statements inside the then function of fetch nor does it set the state of jsonData.
Could you please tell me what could be the problem? I have been googling around for quite a long time trying to find what could be the issue.
EDIT
I tried the async fetch as follows:
componentWillMount() {
console.log("inside componentWillMount");
this.fetchData().done();
}
async fetchData(){
const response = await fetch('http://localhost:3000/listData')
const json = await response.json();
const data = json.url;
console.log('data'+url);
}
Still, the same issue persists.
I am not able to understand why it works with the facebook url and not my local api url
SOLUTION
Thanks to Michael Cheng for pointing me in the right direction.
I found this link :
https://github.com/react-community/create-react-native-app/issues/154 .
Code Fix:
I just replaced the localhost with my ipv4 address as http://myserveripv4address:3000/listData and it worked.
Version:
react-native#0.50.4
Device : Android
some ideas:
if is iOS, you need the remote url to be https or disable this security feature;
You may need to mark your fetch() as async function;
Try to make an async fetch w/o promise.