React Apollo: apollo-cache-persist seems to not be working - apollo

Maybe I misunderstood what this package does, but I assumed that it would read cached responses and help with offline application functionality.
import React from 'react'
import { graphql } from 'react-apollo'
import gql from 'graphql-tag'
export const DATA_QUERY = gql`
query Data {
me {
name
bestFriend {
name
}
}
}
`
const options = () => ({
fetchPolicy: 'cache-only'
})
const withData = graphql(DATA_QUERY, { options })
export const Start = ({ data }) =>
data.loading ? (
'loading!'
) : data.me ? (
<div>
{console.log('data', data)}
<h3>Me: {data.me.name}</h3>
<p>Best friend: {data.me.bestFriend.name}</p>
</div>
) : (
'no data'
)
export default withData(Start)
// index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import registerServiceWorker from './registerServiceWorker';
import { ApolloProvider } from 'react-apollo'
import { ApolloClient } from 'apollo-client'
import { InMemoryCache } from 'apollo-cache-inmemory'
import { HttpLink } from 'apollo-link-http'
import { persistCache } from 'apollo-cache-persist'
const cache = new InMemoryCache()
persistCache({
cache,
storage: window.localStorage,
debug: true
})
export const client = new ApolloClient({
link: new HttpLink({ uri: 'https://v9zqq45l3.lp.gql.zone/graphql' }),
cache
})
ReactDOM.render(
<ApolloProvider client={client}>
<App />
</ApolloProvider>,
document.getElementById('root'));
registerServiceWorker();
I do have the cache in my localStorage
apollo-cache-persist: "{"$ROOT_QUERY.me":{"name":"Bob","bestFriend":{"type":"id","id`enter code here`":"$ROOT_QUERY.me.bestFriend","generated":true}"
When running the above example with fetchPolicy: 'cache-only' the component renders 'no data'. If I do the default fetchPolicy, cache-first, then I get the expected result but I can see the network request is being made.
EDIT: Now works with Daniels answer and this workaround waits for cache to be restored before running the query.
import Start from './Start'
class App extends Component {
state = {
show: false
}
toggle = () =>
this.setState({ show: !this.state.show })
render() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<h1 className="App-title">Welcome to React</h1>
</header>
<br/><br/>
<button onClick={this.toggle}>Show it</button>
<br/><br/>
{this.state.show && <Start />}
</div>
);
}
}

In order to correctly cache and later retrieve the data from the cache, Apollo needs an id (or _id) to work with. If you want to use a different property as the id (like name), you can pass a dataIdFromObject function to your configuration for the in-memory cache:
const cache = new InMemoryCache({
dataIdFromObject: object => {
switch (object.__typename) {
//User is whatever type "me" query resolves to
case 'User': return object.name;
default: return object.id || object._id;
}
}
});

Something like this works, though I wonder if there should be a more elegant solution. Maybe the Retry Link.
https://github.com/apollographql/apollo-cache-persist/issues?utf8=%E2%9C%93&q=is%3Aissue+
export class Index extends Component {
state = {
client: null
}
async componentWillMount() {
const httpLink = new HttpLink({ uri: 'https://v9zqq45l3.lp.gql.zone/graphql' })
const link = ApolloLink.from([ httpLink ])
const cache = new InMemoryCache({
dataIdFromObject: (object) => {
switch (object.__typename) {
// User is whatever type "me" query resolves to
case 'User':
return object.name
default:
return object.id || object._id
}
}
})
await persistCache({
cache,
storage: window.localStorage,
debug: true
})
const client = new ApolloClient({
link,
cache
})
this.setState({ client })
}
render() {
return !this.state.client ? (
null
) : (
<ApolloProvider client={this.state.client}>
<App />
</ApolloProvider>
)
}
}
ReactDOM.render(<Index />, document.getElementById('root'))

Related

TypeError: _bundlr_network_client__WEBPACK_IMPORTED_MODULE_0__ is not a constructor while using metaplex.nfts().uploadMetadata()

I'm using this metaplex javascript SDK for working with nfts on solana blockchain.
While uploading the metadata for an nft, I am getting the following error:
TypeError: _bundlr_network_client__WEBPACK_IMPORTED_MODULE_0__ is not a constructor
Code for connecting to metaplex:
const fromWallet = Keypair.generate();
console.log(fromWallet);
const connection = new Connection(clusterApiUrl("devnet"));
const metaplex = Metaplex.make(connection)
.use(keypairIdentity(fromWallet))
.use(
bundlrStorage({
address: "https://devnet.bundlr.network",
providerUrl: "https://api.devnet.solana.com",
timeout: 60000,
})
);
Uploading metadata function:
async function uploadMetadata() {
try {
const { uri, metadata } = await metaplex.nfts().uploadMetadata({
name: formInput.name,
image: image,
description: formInput.description,
});
console.log(metadata.image);
return uri;
} catch (error) {
console.log(`Error uploading metadata - ${error}`);
}
}
I couldn't understand why I'm getting this error. I tried to hit the function by removing .use(keypairIdentity(fromWallet)) from metaplex configuration. But I am getting another error regarding undefined wallet that way. For the similar configuration of metaplex, mx.nfts().findNftByMint(new PublicKey(address)) is working correctly.
Any help is appreciated. Thank you.
I was facing the exact same issue
I opened an issue on metaplex github repo https://github.com/metaplex-foundation/js/issues/138
so it turned out we need to use the wallet adapter
keypairIdentity & walletAdapterIdentity should work on the browser however only walletAdapterIdentity worked for me .
the link to wallet adapter also created by metaplex is https://github.com/solana-labs/wallet-adapter
update, you just need to wrap you app component
import { WalletAdapterNetwork } from '#solana/wallet-adapter-base';
import { ConnectionProvider, useConnection, useWallet, WalletProvider } from '#solana/wallet-adapter-react';
import { WalletModalProvider, WalletMultiButton } from '#solana/wallet-adapter-react-ui';
import {
GlowWalletAdapter,
PhantomWalletAdapter,
SlopeWalletAdapter,
SolflareWalletAdapter,
TorusWalletAdapter,
} from '#solana/wallet-adapter-wallets';
import { clusterApiUrl, PublicKey } from '#solana/web3.js';
import './App.css';
import '#solana/wallet-adapter-react-ui/styles.css';
import { useEffect, useState, useMemo } from "react";
import { Metaplex } from '#metaplex-foundation/js';
export const App = () => {
return (
<BrowserRouter>
<Context>
<Content />
</Context>
</BrowserRouter>
);
};
const Context = ({ children }) => {
const network = WalletAdapterNetwork.Devnet;
const endpoint = useMemo(() => clusterApiUrl(network), [network]);
const wallets = useMemo(
() => [
new PhantomWalletAdapter(),
new GlowWalletAdapter(),
new SlopeWalletAdapter(),
new SolflareWalletAdapter({ network }),
new TorusWalletAdapter(),
],
[network]
);
return (
<ConnectionProvider endpoint={endpoint}>
<WalletProvider wallets={wallets} autoConnect>
<WalletModalProvider>{children}</WalletModalProvider>
</WalletProvider>
</ConnectionProvider>
);
};
const Content = () => {
const { connection } = useConnection();
const wallet = useWallet();
const metaplex = Metaplex.make(connection);
const [walletAddress, setWalletAddress] = useState('');
const [walletWarning, setWalletWarning] = useState(false);
const [disabled, setDisabled] = useState(false);
useEffect(() => {
const onload = async () => {
await checkIfWalletIsConnected();
}
window.addEventListener('load', onload);
return () => window.removeEventListener('load', onload);
}, []);
return (
<div className='main-app-container'>
<div className='sec-app-container'>
<WalletMultiButton/>
<div className="router-container">
<Routes>
<Route exact path="/" element={<Landing walletWarning={walletWarning} />} />
<Route exact path="/mint_interface"
element={
<MintInterface wallet={wallet} connection={connection} />
} />
</Routes>
</div>
</div>
</div >
);
};
export default App;
and in the MintInterface Component
import { useState } from 'react';
import { Form, Button, Spinner } from 'react-bootstrap';
import { WalletModalProvider, WalletMultiButton } from '#solana/wallet-adapter-react-ui';
import { Metaplex, keypairIdentity, walletAdapterIdentity, bundlrStorage } from '#metaplex-foundation/js';
import { Connection, clusterApiUrl, Keypair } from '#solana/web3.js';
import "./minting.css";
const cluster = "devnet";
function MintInterface({ wallet, connection }) {
const [maturityDate, setMaturityDate] = useState("");
const [quantity, setQuantity] = useState(1);
const [loading, setLoading] = useState(false);
const metaplex = Metaplex.make(connection)
.use(walletAdapterIdentity(wallet))
.use(
bundlrStorage({
address: "https://devnet.bundlr.network",
providerUrl: "https://api.devnet.solana.com",
timeout: 60000,
})
);
);
}
export default MintInterface;
make sure you use walletAdapterIdentity in the configuration

Books API has not been used in project ******** before or it is disabled. Error code 403

I'm trying to migrate to Google Identity Services. When I log a user in and try to get info from their Google Books account I get "error code 403. Books API has not been used in project ****** before or it is disabled"
I checked the project number that was being used before I tried to migrate to Google Identity Services and the project number is totally different from the one stated in the error, and I definitely have the Book API enabled.
I use the following scripts in the react index.html page:
<script src="https://apis.google.com/js/api.js" async defer></script>
script src="https://accounts.google.com/gsi/client" async defer></script>
App.js
import { Routes, Route, Link } from "react-router-dom";
import Home from "./pages/Home";
import CreatePost from "./pages/CreatePost";
import Login from "./pages/Login";
import Logout from "./pages/Logout";
import "./App.css";
import { useEffect, useState } from "react";
function App() {
const [isAuth, setIsAuth] = useState(false);
useEffect(()=>{
console.log(isAuth);
}, [isAuth]);
var tokenClient;
function gisInit() {
tokenClient = window.google.accounts.oauth2.initTokenClient({
client_id: *********,
scope: 'https://www.googleapis.com/auth/books',
});
}
function gapiStart() {
window.gapi.client.init({
}).then(function() {
// fetch the Books API
window.gapi.client.load('books', 'v1');
}).then(function(response) {
console.log('books loaded');
gisInit()
}, function(reason) {
console.log('Error: ' + reason.result.error.message);
});
}
useEffect(()=>{
window.gapi.load('client', gapiStart);
})
return (
<>
<nav>
<Link to="/">Home</Link>
<Link to="/createpost">Create</Link>
{ !isAuth ? <Link to="/login">Login</Link> : <Link to="/logout">Logout</Link> }
</nav>
<Routes>
<Route path="/" element={ <Home /> } />
<Route path="/createpost" element={ <CreatePost /> } />
<Route path="/login" element={ <Login setIsAuth={ setIsAuth } /> } />
<Route path="/logout" element={ <Logout setIsAuth={ setIsAuth } /> } />
</Routes>
</>
);
}
export default App;
Login.js
import {auth, provider} from "../firebase-config";
import { signInWithPopup, GoogleAuthProvider } from "firebase/auth";
import { useNavigate } from "react-router-dom";
import { useEffect } from "react";
// var url = 'books/v1/mylibrary/bookshelves/4/volumes?fields=totalItems, items(id, volumeInfo/title, volumeInfo/authors, volumeInfo/publishedDate, volumeInfo/publisher, volumeInfo/industryIdentifiers, volumeInfo/imageLinks)'; //A local page
function getData(access_token){
// console.log("token " + access_token);
window.gapi.client.setToken({"access_token":access_token});
const fetchUserData = new Promise(function(resolve, reject){
const request = window.gapi.client.request({
'method': 'GET',
// 'path': 'books/v1/mylibrary/bookshelves/4/volumes?fields=totalItems, items(id, volumeInfo/title, volumeInfo/authors, volumeInfo/publishedDate, volumeInfo/publisher, volumeInfo/industryIdentifiers, volumeInfo/imageLinks)'
'path': 'books/v1/mylibrary/bookshelves/4/volumes?fields=totalItems, items(id)',
});
// // Execute the API request.
request.execute( function(response) {
// const obj = response.result;
resolve(response);
reject("Error");
});
});
fetchUserData.then((value)=>{
console.log(value);
}).catch((error)=>{
console.log(error)
});
}
function Login({ setIsAuth }){
useEffect(()=>{
signInWithGoogle();
});
let navigate = useNavigate();
provider.addScope("https://www.googleapis.com/auth/books");
const signInWithGoogle = () =>{
signInWithPopup(auth, provider).then((result)=>{
// This gives you a Google Access Token. You can use it to access the Google API.
const credential = GoogleAuthProvider.credentialFromResult(result);
let token = credential.accessToken;
if(result.user){
setIsAuth(true);
getData(token);
console.log(result.user.displayName);
navigate("/");
}
}).catch((error)=>{
if(error.code === 'auth/popup-closed-by-user'){
setIsAuth(false);
navigate("/");
}
});
}
return (
<>
<p>Logging in...</p>
</>
)
}
export default Login;
What do I have to do to be able to use the right project?
If I swap out the firebase auth and just use the Google Identity Services auth then I can get the information back from the signed in users Google Books account. Looking at the browser network tab the "authorization: Bearer" is the same.
import { useNavigate } from "react-router-dom";
import { useEffect } from "react";
// var url = 'books/v1/mylibrary/bookshelves/4/volumes?fields=totalItems, items(id, volumeInfo/title, volumeInfo/authors, volumeInfo/publishedDate, volumeInfo/publisher, volumeInfo/industryIdentifiers, volumeInfo/imageLinks)'; //A local page
function getData(){
const fetchUserData = new Promise(function(resolve, reject){
const request = window.gapi.client.request({
'method': 'GET',
'path': 'books/v1/mylibrary/bookshelves/4/volumes?fields=totalItems, items(id, volumeInfo/title, volumeInfo/authors, volumeInfo/publishedDate, volumeInfo/publisher, volumeInfo/industryIdentifiers, volumeInfo/imageLinks)'
});
// // Execute the API request.
request.execute( function(response) {
// const obj = response.result;
resolve(response);
reject("Error");
});
});
fetchUserData.then((value)=>{
console.log(value);
}).catch((error)=>{
console.log(error)//error shows an empty array when controller abort called
});
}
function Login({ setIsAuth }){
var tokenClient;
var access_token;
let navigate = useNavigate();
function getToken(){
tokenClient.requestAccessToken();
}
function initGis(){
tokenClient = window.google.accounts.oauth2.initTokenClient({
client_id: *******,
scope: 'https://www.googleapis.com/auth/books',
callback: (tokenResponse) => {
access_token = tokenResponse.access_token;
if(access_token !== undefined){
setIsAuth(true);
getData();
navigate("/");
}
},//end of callback:
});
}
useEffect(()=>{
initGis();
getToken();
});
return (
<>
<p>Logging in...</p>
</>
)
}
export default Login;
All I needed to do to solve the problem is when making the firebase project select the Google Cloud Platform project from the dropdown list.

How to filter elements using React, Graphql and Apollo with es6

I have a React project with a GraphQL using Apollo client. I am trying to figure out how to change the query result based on search text. I implemented query search in backend and its working perfectly.
But I dont know how to set up filter in React using that same query.
Despite there is tutotial on how to filter on https://www.howtographql.com/react-apollo/7-filtering-searching-the-list-of-links/, it doesnt use ES6 and I literaly dont know how to do it. Im stuck on this filter around 10 days.
I will show you my code.
App.js
import React from 'react';
import HeroesDota from './components/HeroesDota';
import Search from './components/HeroSearch'
import { ApolloProvider } from '#apollo/react-hooks';
import { ApolloClient } from "apollo-client";
import { InMemoryCache } from "apollo-cache-inmemory";
import { HttpLink } from "apollo-link-http";
const cache = new InMemoryCache();
const link = new HttpLink({
uri: "http://localhost:8000/graphql/"
});
const client = new ApolloClient({
cache,
link
});
const App = () => {
return (
<ApolloProvider client={client}>
<Search />
<HeroesDota />
</ApolloProvider>
)};
export default App;
HeroesDota.js (compoenent)
import React from 'react'
import gql from "graphql-tag";
import { useQuery } from '#apollo/react-hooks';
import '../index.css'
import styled from 'styled-components';
const Images = styled.img`
margin:0;
border: 3px solid #288eea;
display: inline;
width: 90px;
height: 50px;
`
const HEROES_DOTA2 = gql`
query {
heroes {
name
heroType
image
}
}
`;
const HeroesDota = () => {
const { loading, error, data } = useQuery(HEROES_DOTA2);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error :(</p>;
return data.heroes.map(({name, heroType, image }) => (
<div className="row" key={Math.random() + 1}>
<div className="column">
<button className="button-hero"><Images className="hero_images" src= {`${image}`} alt={name}></Images></button>
<div className="hero_info">{name} - {heroType}</div>
</div>
</div>
));
}
export default HeroesDota;
HeroSearch.js (compoenent that doesnt work as I expected)
import React, { useState } from 'react'
import gql from "graphql-tag";
import { withApollo } from 'react-apollo'
import Hero from './HeroesDota'
import '../index.css'
const SEARCH_HEROES = gql`
query ($search: String) {
heroes (search: $search) {
id
name
}
}
`;
const Search = () => {
const [heroes, setHeroes] = useState([])
const [search, setSearch] = useState('')
const _executeSearch = async () => {
const { search } = search
const result = await this.props.client.query({
query: SEARCH_HEROES,
variables: { search },
})
const heroes = result.data.heroes.name
setHeroes({ heroes })
}
return (
<div>
<div>
Search
<input
type='text'
onChange={e => setSearch({ search: e.target.value })}
/>
<button onClick={() => _executeSearch()}>OK</button>
</div>
{heroes.map((hero, index) => (
<Hero key={hero.id} hero={hero} index={index} />
))}
</div>
)
}
export default withApollo(Search)
After U oress OK button to exectue search i get following error.
Unhandled Rejection (ReferenceError): Cannot access 'search' before initialization.
If I try to do something similar like I did in Component HeroesDota i still cant make it.
Does anyone know how to filter query in React using es6, not class based compoenent like they did on this tutorial.
Thanks
const Search = (props) => {
props.client.query()
...
}
or
const Search = ({ client }) => {
client.query()
...
}
Although I would also recommend you use the the new hooks syntax.

Keep user session logged in when page refreshed in vue js

I'm create user login page in vue js and consuming data from django with axios. I have utilized jwt to create token session in client-side
The problem is the session is not saved when the page is refreshed. I have frustated because it. This is my source code :
In '../src/store/modules/auth.js'
import Vue from 'vue'
import Axios from 'axios'
import 'es6-promise/auto'
// In order that Axios work nice with Django CSRF
Axios.defaults.xsrfCookieName = 'csrftoken'
Axios.defaults.xsrfHeaderName = 'X-CSRFToken'
const state = {
authUser: {},
users: [],
isAuthenticated: false,
jwt: localStorage.getItem('token'),
endpoints: {
obtainJWT: 'http://127.0.0.1:8000/api/auth/obtain_token/',
refreshJWT: 'http://127.0.0.1:8000/api/auth/refresh_token/',
baseUrl: 'http://127.0.0.1:8000/api/auth/',
register: 'http://127.0.0.1:8000/signup/'
}
}
const mutations = {
setAuthUser: (state, {
authUser,
isAuthenticated
}) => {
Vue.set(state, 'authUser', authUser)
Vue.set(state, 'isAuthenticated', isAuthenticated)
},
updateToken: (state, newToken) => {
localStorage.setItem('token', newToken);
state.jwt = newToken;
},
removeToken: (state) => {
localStorage.removeItem('token');
state.jwt = null;
},
}
const actions = {
refreshToken(){
const payload = {
token: this.state.jwt
}
Axios.post(state.endpoints.refreshJWT, payload)
.then((response)=>{
this.commit('updateToken', response.data.token)
})
.catch((error)=>{
console.log(error)
})
}
}
export default {
state,
mutations,
actions,
}
In '../src/store/index.js'
import Vue from 'vue'
import Vuex from 'vuex'
import axios from 'axios'
import auth from './modules/auth'
Vue.use(Vuex)
// Make Axios play nice with Django CSRF
axios.defaults.xsrfCookieName = 'csrftoken'
axios.defaults.xsrfHeaderName = 'X-CSRFToken'
export default new Vuex.Store({
modules: {
auth
},
})
In '../src/components/login.vue'
<template>
<div class="login">
<form>
<label for="username">Username</label>
<input
type="text"
name="username"
v-model="username"
/><br>
<label for="password">Password</label>
<input
type="password"
name="password"
v-model="password"
/><br>
<input
type="button"
#click="login()"
value="Login"
/>
</form>
</template>
<script>
import axios from 'axios'
/* eslint-disable */
export default {
name: 'Login',
data(){
return {
username: '',
password: ''
}
},
methods: {
login(){
const payload = {
username: this.username,
password: this.password
}
axios.post(this.$store.state.auth.endpoints.obtainJWT, payload)
.then((response) => {
this.$store.commit('updateToken', response.data.token)
this.$store.commit('setAuthUser',
{
authUser: response.data,
isAuthenticated: true
}
)
this.$router.push({path: 'dashboard-user/id/list-vendor'})
})
.catch((error) => {
//NOTE: erase this when production
console.log(error);
console.debug(error);
console.dir(error);
alert("The username or password is incorrect");
})
}
}
}
</script>
In 'main.js'
import Vue from 'vue'
import VueRouter from 'vue-router'
import App from './App.vue'
import 'tachyons'
import routes from './routes'
import './styles.css'
import store from '#/store'
Vue.config.productionTip = false
Vue.use(VueRouter)
import '#/assets/fonts/all.css';
const router = new VueRouter({
mode: 'history',
routes
})
router.beforeEach((to, from, next) => {
// to and from are both route objects. must call `next`.
if(to.fullPath === '/dashboard-user/id/list-vendor') {
if(!store.state.jwt) {
next('/login')
}
}
if(to.fullPath === '/login') {
if(store.state.jwt) {
next('/dashboard-user/id/list-vendor')
}
}
next();
})
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
Since use register auth as a module, you should use store.state.auth.jwt instead of store.state.jwt
router.beforeEach((to, from, next) => {
// to and from are both route objects. must call `next`.
if(to.fullPath === '/dashboard-user/id/list-vendor') {
if(!store.state.auth.jwt) {
next('/login')
}
}
if(to.fullPath === '/login') {
if(store.state.auth.jwt) {
next('/dashboard-user/id/list-vendor')
}
}
next();
})

How to properly unit test login and local storage

After 3 days researching and not ariving anywhere, I decided to ask here for someone that already have similar experience or can point a better path to follow.
The better SO question I've found was this but left some questions in air: React - how to test form submit?
Since I'm begginer I believe I may getting something wrong, but no sure exactly which. If it's the way I build the components or even test concept itself.
I have the following case:
When a user logins in, it calls API (mock) then save token result (when successful) to localStorage (mock)
When user is already logged in, it gets redirected to homepage
My code until now:
Login Component
class Login extends React.Component {
constructor(props) {
super(props);
this.state = {
email: '',
password: ''
};
this.handleSubmit = this.handleSubmit.bind(this);
this.handleChange = this.handleChange.bind(this);
}
handleSubmit(e) {
e.preventDefault();
this.props.sendLoginRequest(this.state).then(
({data}) => {
console.log(data);
},
(data) => {
console.error(data);
}
);
}
handleChange(e) {
this.setState({ [e.target.name]: e.target.value });
}
render() {
return (
<div id='auth-container' className='login'>
<Form onSubmit={this.handleSubmit}>
<FormGroup controlId='emailaddress'>
<InputGroup bsSize='large'>
<InputGroup.Addon>
<Icon glyph='icon-fontello-mail' />
</InputGroup.Addon>
<FormControl
autoFocus
className='border-focus-blue'
type='email'
placeholder='email#fixdin.com'
name='email'
onChange={this.handleChange}
value={this.state.email} />
</InputGroup>
</FormGroup>
<FormGroup controlId='password'>
<InputGroup bsSize='large'>
<InputGroup.Addon>
<Icon glyph='icon-fontello-key' />
</InputGroup.Addon>
<FormControl
className='border-focus-blue'
type='password'
placeholder='password'
name='password'
onChange={this.handleChange}
value={this.state.password} />
</InputGroup>
</FormGroup>
</Form>
</div>
)
}
}
Login.propTypes = {
sendLoginRequest: React.PropTypes.func.isRequired
}
authAction.js
import createApi from '../services/api';
import { saveToken } from '../services/session';
export function sendLoginRequest(loginData) {
return dispatch => {
const api = createApi();
const loginPromise = api.post('auth/', loginData);
loginPromise.then(
({ data }) => {
saveToken(data.token);
}
);
return loginPromise;
}
}
API..js
import axios from 'axios';
import { isAuthenticated, getToken } from './session';
export const BASE_URL = 'http://localhost:8000/api/v1/';
export default function createAPI() {
let auth = { }
if (isAuthenticated()) {
auth = {
Token: getToken()
}
}
return axios.create({
baseURL: BASE_URL,
auth: auth
});
};
session.js
const TOKEN_KEY = 'token';
export function saveToken(value)
{
localStorage.setItem(TOKEN_KEY, value);
}
export function getToken()
{
return localStorage.getItem(TOKEN_KEY)
}
export function isAuthenticated() {
return getToken() !== null;
}
My test stack is Mocha/Chai/Enzyme/sinon and it's defined
setup.js
var jsdom = require('jsdom');
class LocalStorageMock {
constructor() {
this.store = {};
}
clear() {
this.store = {};
}
getItem(key) {
return this.store[key];
}
setItem(key, value) {
this.store[key] = value.toString();
}
};
if(!global.document) {
global.document = jsdom.jsdom('<!doctype html><html><body></body></html>');
global.window = document.defaultView;
global.navigator = {userAgent: 'node.js'};
global.localStorage = new LocalStorageMock;
}
login-test.js
import React from 'react';
import sinon from 'sinon';
import { mount, shallow } from 'enzyme';
import { expect } from 'chai';
import { Provider } from 'react-redux';
import axios from 'axios'
import moxios from 'moxios'
import store from './../src/store';
import LoginPage from './../src/auth/components/Login';
describe('Login', () => {
beforeEach(function () {
moxios.install(axios)
})
afterEach(function () {
moxios.uninstall(axios)
})
it('should call action on form submit', () => {
const submitRequest = sinon.stub(LoginPage.prototype, 'handleSubmit').returns(true);
const wrapper = mount(<Provider store={store}><LoginPage /></Provider>);
wrapper.find('form').simulate('submit');
expect(submitRequest.called).to.be.true;
submitRequest.restore();
});
it('should save token on succesfull login', () => {
const wrapper = mount(<Provider store={store}><LoginPage /></Provider>);
const emailInput = wrapper.find('input[type="email"]');
const passInput = wrapper.find('input[type="password"]');
const form = wrapper.find('form');
emailInput.value = "valid#email.com";
passInput.value = '123456789';
form.simulate('submit'); // Should I use submit button instead???
moxios.wait(function () {
let request = moxios.requests.mostRecent()
request.respondWith({
status: 200,
response:
{ Token: 'validToken' }
}).then(function () {
expect(localStorage.getItem('Token')).to.equal('validToken');
});
});
});
});
Above test does not pass, since it returns false for submitRequest.called and second test fails with error "Cannot read property 'respondWith' of undefined". I'm not sure how to fix and more, I'm not sure if I idealized it right!!
When doing a lot of research about it, I've seen examples with tests specific for component method call + isolated action test.
So...
When I think about "click login and save token" I'm overthinking a unit test? There's a better way to test things like that? Maybe separate some concerns?
This is the correctly way to test if a form submit invoke its callback? If so, why sinon is not working there?
This is the correctly way to mock + test api call to login and localStorage? If so, why Moxios is not working properly? It keeps giving me that mostRecent() is undefined.
If no, to question 2 and 3, where can I find a valid and working example of how to properly test cited behavior?
Thanks in advance.