Cypress using actions from Pinia Vue3 - cypress

I was learning some cypress from this video: https://www.youtube.com/watch?v=03kG2rdJYtc
I'm interested with he's saying at 29:33: "programatic login"
But he's using vue2 and Vuex.
My project is created with Vite and the state management is Pinia.
So how can I do a programatic login using the pinia action?
For example the welcome logged in user should see dashboard:
describe('Welcome', () => {
it('logged in user should visit dashboard', () => {
// login
cy.visit('/')
cy.url().should('contain', '/dashboard')
})
})
And my userStore:
export const useUserStore = defineStore({
id: 'user',
state: () => ({
username: ref(useLocalStorage('username', null)),
}),
getters: {
isLoggedIn: (state) => state.username !== null,
},
actions: {
login(username, password) {
return useAuthLoginService(username, password)
.then((response) => {
this.username = response.username
})
.catch((error) => {
return Promise.reject(new Error(error))
})
},
},
})
How can I call the login action on the cypress test?
For now as a workaround I'm writing on a localstorage like:
localStorage.setItem('username', 'user')
And it works fine, because userStore catch this item from localstorage and passes like it's logged in... But I don't like this solution, seems fragile, and I'd like to use the action which is made for login users.
Another thing I tried is adding the app variable inside window but it doesn't work for me... don't understand why...
on main.js
The video shows that code:
const vue = new Vue({...})
if(window.Cypress){
window.app = app
}
In my case it's:
const app = createApp(App)
if(window.Cypress){
window.app = app
}
But in cypress tests the window.app it's undefined... I don't know how I would access to userStore using this... like it was vuex.

Using the Pinia demo app as an example:
The store is initialized in App.vue. Add a reference to the newly created store(s) for Cypress to use
export default defineComponent({
components: { Layout, PiniaLogo },
setup() {
const user = useUserStore()
const cart = useCartStore()
if (window.Cypress) {
window.store = {user, cart) // test can see window.store
}
...
In the test
let store;
describe('Pinia demo with counters', () => {
beforeEach(() => {
cy.viewport(1000, 1000)
cy.visit(`http://localhost:${PORT}`)
.then(win => store = win.store) // get app's store object
})
it('works', () => {
cy.wait(500) // wait for the JS to load
.then(() => store.cart.addItem('Cypress test item')) // invoke action
.then(() => {
const item1 = store.cart.items[0] // invoke getter
cy.wrap(item1)
.should('have.property', 'name', 'Cypress test item') // passes
})
The login action is asynchronous, so return the promise to allow Cypress to wait.
// user.js
async login(user, password) {
const userData = await apiLogin(user, password)
this.$patch({
name: user,
...userData,
})
return userData // this returns a promise which can awaited
},
// main.spec.js
describe('Pinia demo with counters', () => {
beforeEach(() => {
cy.viewport(1000, 1000)
cy.visit(`http://localhost:${PORT}`).then(win => {
store = win.store
// default name in store before login
cy.wrap(store.user.name).should('eq', 'Eduardo')
// logging in
store.user.login('ed', 'ed').then(() => { // wait for API call
cy.wrap(store.user.name).should('eq', 'ed')
})
})
})
Alternatively, wait for the name to change on the page
// main.spec.js
cy.visit(`http://localhost:${PORT}`).then(win => {
store = win.store
// default name in store
cy.wrap(store.user.name).should('eq', 'Eduardo')
// logging on
store.user.login('ed', 'ed')
cy.contains('Hello ed') // waits for name on page to change
.then(() => {
cy.wrap(store.user.name).should('eq', 'ed')
})
})

Related

Cypress.Cookies.preserveOnce('session_id') isn't working when called in afterEach hook

I'm working with cypress tests and I want to avoid having to log in before each test. so, I wanted to preserve the cookies in each test file.
The log statement in the afterEach hook is triggered, however cookies are cleared in the second testcase.
describe('Users Page Scenarios', () => {
before(() => {
myApp.pages.Login.navigate();
myApp.pages.Login.login(
credentials.globalAdmin.email,
credentials.globalAdmin.password
);
});
beforeEach('navigate to users page before each test', () => {
myApp.sharedComponents.Header.navigateToUsers();
});
afterEach(() => {
Cypress.Cookies.preserveOnce('session_id');
cy.log('test');
});
describe('Users List', () => {
it('Should redirect the user to users page after clicking on users in the navigation header', () => {
cy.url().should('eq', `${Cypress.config().baseUrl}/user`);
});
})
describe('New User Creation', () => {
it('Should open new user modal after clicking on invite administrator', () => {
myApp.pages.Users.UsersList.inviteAdministrator();
cy.url().should('eq', `${Cypress.config().baseUrl}/user/new`);
});
it('Should create a new user successfully', () => {
myApp.pages.Users.UsersList.inviteAdministrator();
myApp.pages.Users.UsersInfo.createNewUser(user.generateUser());
})
});
The docs indicate that Cypress.Cookies.preserveOnce('session_id') is used in beforeEach().
Looks like after is too late.
describe('Dashboard', () => {
before(() => {
// log in only once before any of the tests run.
// your app will likely set some sort of session cookie.
// you'll need to know the name of the cookie(s), which you can find
// in your Resources -> Cookies panel in the Chrome Dev Tools.
cy.login()
})
beforeEach(() => {
// before each test, we can automatically preserve the
// 'session_id' and 'remember_token' cookies. this means they
// will not be cleared before the NEXT test starts.
//
// the name of your cookies will likely be different
// this is an example
Cypress.Cookies.preserveOnce('session_id', 'remember_token')
})
If you have localStorage or sessionStorage to preserve, or you have not identified all cookies correctly, try with cy.session()
beforeEach(() => { // must be beforeEach()
cy.session('mySession', () => { // preserves localStorage, sessionStorage, cookies
myApp.pages.Login.navigate();
myApp.pages.Login.login(...); // only called once (despite beforeEach())
})
})

Get vuex store state after dispatching an action

I'm creating a chat application in Laravel 6 + Vue + Vuex. I want make a call to vuex store and get a state after a dispatch actions is complete and then I want to do some processing on that state in my vue component.
In ChatWindow component
mounted: function () {
this.$store.dispatch('setContacts').then(() => {
console.log('dispatch called')
// I want to call the getter here and set one of the data property
});
}
action.js
setContacts: (context) => {
axios.post('/users').then(response => {
let users = response.data;
// consoled for testing
console.log(users);
context.commit('setContacts', users);
});
}
mutators.js
setContacts: (state, users) => {
state.contacts = users;
},
Please see the screenshot below. The then method of dispatch is running before setContacts in action.js.
I need to call the getter after completing dispatch action. (which will effectively set the contacts state). Then, I want to get the contacts through getContacts getter like this.
getters.js
getContacts: (state) => {
return state.contacts;
}
I also tried calling computed property in then in mounted and it didn't work. Also, shouldn't 'dispatch called' in mounted run after console.log of setContacts in action.js as it is in then method? Thanks!
Maybe you could wrap axios call inside another promise.
return new Promise((resolve, reject) => {
axios.post('/users')
.then(response => {
let users = response.data;
// consoled for testing
console.log(users);
context.commit('setContacts', users);
resolve('Success')
})
.catch(error => {
reject(error)
})
})
And then
this.$store.dispatch('setContacts')
.then(() => {
console.log('dispatch called')
console.log('getter ', this.$store.getters.contacts)
});
Let me know what happens. It was working for a small demo that I tried.

passing component state to redux-form onSubmit

novice user of redux-form here. I have a signin modal that has 2 different operations: login and register. The role (stored in component state) will be login by default, and the user will be able to click a button to change it to register.
Where I'm stuck, is that I want to pass that piece of state to the onSubmit() function, so that I can dispatch the correct actions depending on if the user is trying to login or register.
My thinking was that I could pass down this piece of state called signInType as a prop to the function. Of course, it is not working as I would have expected. I can pass in a prop via the reduxForm HOC, but from that function I cannot access the component's state.
Here are the relevant parts of my component to help understand what my end goal is here:
const [signInType, setSignInType] = useState('login')
const onSubmit = (data, dispatch, props) => {
console.log('props: ', props);
if (props.signInType === 'login') {
return (
api.post('/Login', data)
.then(json => {
const response = JSON.parse(json.d)
if (!response.userid) {
console.error(response.message)
dispatch(emailLoginFailure(response.message))
return response.message
}
LogRocket.identify(response.userid, {
email: data.email,
})
dispatch(emailLoginSuccess(response))
})
.catch(err => {
console.error(err)
dispatch(emailLoginFailure(err))
})
)
} else if (props.signInType === 'register') {
return (
api.post('/RegisterByEmail', {
email: data.email,
password: data.password,
utm_source: "Development",
utm_medium: "email",
utm_campaign: "Campaign Test",
utm_term: "N/A",
utm_content: "123",
utm_date: "2019-02-11 12:25:36"
})
.then(json => {
const response = JSON.parse(json.d)
if (!response.userid) {
console.error(response.message)
dispatch(emailRegisterFailure(response.message))
return response.message
}
// LogRocket.identify(response.userid, {
// email: data.email,
// })
dispatch(emailRegisterSuccess(response))
})
.catch(err => {
console.error("Unable to register email:", err)
})
)
} else {
console.error("error: No signin type?")
}
}
Thanks for the help :)
Such login/register flow I prefer handling it with different components, in order to respect and follow SRP.
Also, I'm not sure how do you organize your components, but here's how I deal with such a scenario:
Your Modal:
* It will be responsible only for rendering Login or Register forms.
const Modal = () => {
const [signInType, ] = useState('login')
const isLogin = signInType === 'login'
return <>
{ isLogin ? <LoginForm /> : <RegisterForm /> }
<button onClick={() => setSignInType(isLogin ? 'register' : 'login')}>
{ isLogin ? 'Sign up' : 'Sign in' }
</button>
</>
}
LoginForm:
* Now you can pass your login action to onSubmit prop. Login will be your presentation component, while LoginForm decorates Login with reduxForm HOC.
export default reduxForm({
form: 'login',
onSubmit: data => {}
})(Login)
RegisterForm:
* Here we follow the same idea as LoginForm.
export default reduxForm({
form: 'register',
onSubmit: data => {}
})(Register)

Can the completion of one async call be sequenced before the start of another using useEffect?

I'm trying to use useEffect in my React app but also refactor things more modularly. Shown below is the heart of actual working code. It resides in a Context Provider file and does the following:
1. Calls AWS Amplify to get the latest Auth Access Token.
2. Uses this token, in the form of an Authorization header, when an Axios GET call is made to an API Endpoint.
This works fine but I thought it would make more sense to move Step #1 into its own useEffect construct above. Furthermore, in doing so, I could then also store the header object as its own Context property, which the GET call could then reference.
Unfortunately, I can now see from console log statements that when the GET call starts, the Auth Access Token has not yet been retrieved. So the refactoring attempt fails.
useEffect(() => {
const fetchData = async () => {
const config = {
headers: { "Authorization":
await Auth.currentSession()
.then(data => {
return data.getAccessToken().getJwtToken();
})
.catch(error => {
alert('Error getting authorization token: '.concat(error))
})
}};
await axios.get('http://127.0.0.1:5000/some_path', config)
.then(response => {
// Process the retrieved data and populate in a Context property
})
.catch(error => {
alert('Error getting data from endpoint: '.concat(error));
});
};
fetchData();
}, [myContextObject.some_data]);
Is there a way of refactoring my code into two useEffect instances such that the first one will complete before the second one starts?
You could hold the config object in a state. This way you can separate both fetch calls and trigger the second one once the first one finished:
const MyComponent = props => {
const myContextObject = useContext(myContext);
const [config, setConfig] = useState(null);
useEffect(() => {
const fetchData = async () => {
const config = {
headers: {
Authorization: await Auth.currentSession()
.then(data => {
return data.getAccessToken().getJwtToken();
})
.catch(error => {
alert("Error getting authorization token: ".concat(error));
})
}
};
setConfig(config);
};
fetchData();
}, [myContextObject.some_data]);
useEffect(() => {
if (!config) {
return;
}
const fetchData = async () => {
await axios
.get("http://127.0.0.1:5000/some_path", config)
.then(response => {
// Process the retrieved data and populate in a Context property
})
.catch(error => {
alert("Error getting data from endpoint: ".concat(error));
});
};
fetchData();
// This should work for the first call (not tested) as it goes from null to object.
// If you need subsequent changes then youll have to track some property
// of the object or similar
}, [config]);
return null;
};

substate.get() is not a function using React Boilerplate

I have a component called Login, and these selectors:
const selectLogin = () => (state) => state.get('login');
const selectUser = () => createSelector(
selectLogin(),
(loginState) => loginState.get('user')
);
Here's what state looks like for the "login" component:
login: {
user: {
id: 206
}
}
In another component, I want to select the "user" object.
At the top of my file, I have
import { createStructuredSelector } from 'reselect';
import {
selectLogin,
selectUser
} from 'containers/Login/selectors';
const mapStateToProps = createStructuredSelector({
login: selectLogin(),
user: selectUser(),
});
When I use "selectUser()", I get "loginState.get is not a function".
If I remove all references to "selectUser()", I can access this.props.login.user. That works for me, but I want to know why I can't select from within the "login" state. The examples use the same "substate" convention in the selector, and they work. Any ideas?
Is this another component in another route?
You have to manually inject reducers and sagas required for the page in each route.
In route.js, loadReducer and inject it to the page, something like this:
{
path: '/projects/add',
...
getComponent(nextState, cb) {
const importModules = Promise.all([
System.import('containers/Project/reducer'),
System.import('containers/Login/reducer')
...
]);
const renderRoute = loadModule(cb);
importModules.then(([projectReducer, loginReducer ...]) => {
injectReducer('projects', projectReducer.default);
injectReducer('login', projectReducer.default);
renderRoute(component);
});
importModules.catch(errorLoading);
},

Resources