My question is, how do you override the variable in the async function which is out of the scope of this?
I read here that the problem is the lack of a callback. After adding the callback, the variable outside the scope in which it was changed (but still the variable itself is in the correct scope) returns "undefined". What am I doing wrong?
Test:
const savedVariableCallback = (variable) =>
console.log(`Variable saved ${variable}`);
describe(() => {
...
it("Sample input type", () => {
let fixValue;
cy.fixture("example.json").then(({ email }) => {
actionsPage
.selectSampleInput()
.then((input) => {
checkAmountOfElements(input, 1);
checkVisiblity(input);
})
.type(email)
.then((input) => {
checkIfValue(input, email);
fixValue = "Nice work";
savedVariableCallback(fixValue);
});
});
cy.log(`fixValue is: ${fixValue}`);
});
})
I expect the first log to show Variable saved Nice work and the second log to show fixValue is: Nice work for variables. But for now, I get in the first log Variable saved Nice work but in second I get undefined.
I want to have that variable to be accessible in it() method scope.
Edit: Since the reference didn`t work I suggest approaching it with an allias
const savedVariableCallback = (variable) =>
console.log(`Variable saved ${variable}`);
describe(() => {
...
it("Sample input type", () => {
cy.fixture("example.json").then(({ email }) => {
actionsPage
.selectSampleInput()
.then((input) => {
checkAmountOfElements(input, 1);
checkVisiblity(input);
})
.type(email)
.then((input) => {
checkIfValue(input, email);
let fixValue = "Nice work";
savedVariableCallback(fixValue);
cy.wrap(fixValue).as('fixValue')
});
});
cy.get('#fixValue')
.then(fixValue => {
cy.log(`fixValue is: ${fixValue.value}`);
})
});
})
If you change
cy.log(`fixValue is: ${fixValue}`)
to
console.log(`fixValue is: ${fixValue}`)
you can see the order of logging
fixValue is: undefined
Variable saved Nice work
so cy.log() grabs the value of fixValue at the time the command is added to the queue, that is before cy.fixture() has run, even though it actually runs after the cy.fixture().
You can defer it until cy.fixture() is complete by adding another .then()
cy.fixture("example.json").then(data => {
...
})
.then(() => {
// enqueued after the fixture code
cy.log(`fixValue is: ${fixValue}`) // now it sees the changed value
})
but of course everything downstream that needs to use fixValue must be inside the .then() callback.
You can also defer the "grabbing" of the value
cy.then(() => cy.log(`fixValue is: ${fixValue}`))
or split cy.fixture() into a before(), which will resolve the variable before the test begins
let fixValue;
before(() => {
cy.fixture("example.json").then(data => {
...
fixValue = ...
})
})
it('tests', () => {
cy.log(`fixValue is: ${fixValue}`) // sees the changed value
})
Related
I would like to write a function that can be called whenever I set a default value in my Schema.
At the moment I keep repeating the same code as default value (getting the default SVG from its collection) for each of my parts (e.g. body, ear, head, arm) - which works and looks like this:
arm: { type: partSchema, default: function (){
Arm.findOne({ name: 'default' })
.then(pic => {
this.model.arm = pic;
Pattern.findOne({ name: 'default' })
.then(pic => {
this.model.arm.pattern = pic;
})
.catch(err => {
console.log(err);
});
})
.catch(err => {
console.log(err);
});
What I would like to do is something like this:
function defaultPart(user, part) {
//will turn e.g. part='arm' to 'Arm' so my function can find the apropriate model
var PartModel = mongoose.model(`${part.charAt(0).toUpperCase() + part.slice(1)}`);
PartModel.findOne({ name: 'default' })
.then(pic => {
//model.part does not exist and will not be filled with data, logically
//I'd instead like the function to exchange part with 'arm'
this.model.part = pic;
Pattern.findOne({ name: 'default' })
.then(pic => {
//same concept as above
this.model.part.pattern = pic;
})
.catch(err => {
console.log(err);
});
})
.catch(err => {
console.log(err);
});
}
and then call it within my Schema
arm: {
type: partSchema, default: function () {
defaultPart(user, 'arm');
}},
I've tried to look for answers within Stackoverflow, but the answers mostly revolve around dynamically creating queries like these:
Is there a way to use a variable for the field name in a find() in mongoose?
However I'm not interested in querying my DB, I want to set values within, or rather assign their keys dynamically.
I feel like I'm missing something very basic here, something like a template string in Javascript, but I just can't figure out what it is.
Is my idea feasable or will I be stuck with copy-pasting it to all my Schemas (total: 15) separatly?
I am trying to verify if the user is inside that list that I capture by axios, the issue is that I have used the FILTER option but it always returns undefined or [], being that if the user exists in that array.
I can't think what else to do, because I validate if it is by console.log() the variable with which I ask and if it brings data.
created() {
this.getStagesDefault()
this.getSalesman()
this.getStagesAmountByUser()
},
methods: {
async getSalesman(){
const { data } = await axios.get('salesman')
this.employees = data.data
},
getStagesAmountByUser(){
console.log(this.user['id'])
var objectUser = this.employees.filter(elem => {
return elem.id === this.user['id']
})
console.log(objectUser)
},
Console
Vue data
The method getSalesman is asynchronous, meaning that getStagesAmountByUser will start executing before getSalesman finishes.
Two ways to fix the problem:
Await the getSalesman method, but you have to make the created method async as well. Change the code as follows:
async created() {
this.getStagesDefault()
await this.getSalesman()
this.getStagesAmountByUser()
}
Attach a .then to the getSalesman function, and start the next one inside the .then. Change the code as follows:
created() {
this.getStagesDefault()
this.getSalesman().then(() => this.getStagesAmountByUser())
}
getSalesman is an async method. At the time of the filter, the array being filtered is still empty.
this.getSalesman() // this runs later
this.getStagesAmountByUser() // this runs right away
Have the methods run sequentially by awaiting the async method:
await this.getSalesman()
this.getStagesAmountByUser()
You can avoid the inefficient clientside filtering if you pass the id to the backend and only select by that id.
Additionally, created only gets called once unless you destroy the component which is also inefficient, so watch when user.id changes then call your method again.
Plus don't forget you must wrap any async code in a try/catch else you will get uncaught errors when a user/salesman is not found etc, you can replace console.error then with something which tells the user the error.
{
data: () => ({
employee: {}
}),
watch: {
'user.id' (v) {
if (v) this.getEmployee()
}
},
created() {
this.getEmployee()
},
methods: {
getEmployee() {
if (typeof this.user.id === 'undefined') return
try {
const {
data
} = await axios.get(`salesman/${this.user.id}`)
this.employee = data.data
} catch (e) {
console.error(e)
}
}
}
}
I am developing a React Native application and am facing the following error:
I have defined a useRef which stores the doc ID from a firebase collection. But when I call that variable after it has been defined, the .current value returns a blank string.
db.collection('users').onSnapshot((snapshot) => {
snapshot.docs.map((doc) => {
if (doc.data().email === auth.currentUser?.email) {
bidId.current = doc.id
console.log(bidId.current)
}
})
})
The above code returns the expected value. However, when I call the variable outside this db.collection loop, I get the following value:
But calling the bidId.current returns a blank string.
Please can someone help me with this. Thanks!
Actually this is what happens:
db.collection('users').onSnapshot((snapshot) => {
snapshot.docs.map((doc) => {
if (doc.data().email === auth.currentUser?.email) {
bidId.current = doc.id
// This line gets executed after some time!
console.log(bidId.current)
}
})
})
// This gets executed first! (The value has not been stored yet!)
console.log(bidId.current);
Using the "useState" hook instead of "useRef" will solve the issue. Consider the following code:
const [BidId, setBidId] = useState<string | null>(null);
// This will store the value...
useEffect(() => {
db.collection('users').onSnapshot((snapshot) => {
snapshot.docs.map((doc) => {
if (doc.data().email === auth.currentUser?.email) {
setBidId(doc.id);
}
})
})
}, []);
// Here you can access the value
useEffect(() => {
if(BidId !== null)
console.log(BidId);
}, [BidId]);
// You can also return the component like the following:
return (<View>The Bid ID is: {BidId !== null ? BidId : "Loading..."}</View>);
Your useEffect basically says that whenever pageRef changes, call this function. If done outside, it will call do your tasks on every render instead of doing the whenever pageRef values is changed. Also, in initial renders, it may give undefined values.
You can only return a function in useEffect which basically says that before running the same next time, run this function before.
Try (currentUser without the '?' query character):
if (doc.data().email === auth.currentUser.email) {
bidId.current = doc.id
console.log(bidId.current)
}
The Problem
I need a way to cy.get elements by trying multiple selectors with retries/timeouts. I'm aware this is not really the intended way to use Cypress and I've read Conditional Testing
but unfortunately this is a requirement. It's intended to provide fallback identifiers.
My attempts so far haven't worked well. I've tried using 'normal' javascript promises with async/await but then cypress complains about mixing promises and commands.
I've commented some test cases below with my expectations and what actually happens.
Example HTML
<!-- example.html -->
<button id="button-1" onclick="this.style.color='red'">my button</button>
Test Cases / My Function
beforeEach(() => {
cy.visit('./example.html')
})
function getWithMultipleSelectors(selectors) {
return new Cypress.Promise(resolve => {
cy.wrap(selectors).each(selector => {
getWithRetries(selector).then(element => {
if (element) resolve(element)
})
// how do I exit out of this .each() early?
// I only know if I found something inside .then() so I can't just do `return false`
})
})
}
function getWithRetries(selector, retries = 3) {
return new Cypress.Promise(resolve => {
cy.wrap([...Array(retries).keys()]).each(attempt => {
cy.log(`attempt nr ${attempt}`)
const element = cy.$$(selector)
cy.log(element, selector)
if (element.length === 1) {
resolve(element[0])
return false // ends .each()
}
cy.wait(1000) // wait before next attempt
})
})
}
// just a sanity check that the button can indeed be found
it('normal get function finds #button-1', () => {
cy.get('#button-1').should('exist').click()
})
// to see what happens if you check existence of null or undefined
// as expected they are considered to not exist
it('cy.wrap() null and undefined', () => {
cy.wrap(undefined).should('not.exist')
cy.wrap(null).should('not.exist')
})
// ends with "expected undefined to exist in the DOM" which somehow passes
// but fails when trying to click()
it('finds the button with one selector', () => {
getWithMultipleSelectors(['#button-1']).then(element => {
cy.wrap(element).should('exist').click()
})
})
// ends with "expected undefined to exist in the DOM" which somehow passes
// but fails when trying to click()
it('finds the button with two selectors', () => {
getWithMultipleSelectors(['#does-not-exist', '#button-1']).then(element => {
cy.wrap(element).should('exist').click()
})
})
// this test should FAIL but it doesn't
it('fails if no selector matches', () => {
getWithMultipleSelectors(['#does-not-exist']).then(element => {
cy.wrap(element).should('not.exist').click()
})
})
Versions Used
Cypress package version: 7.5.0
Cypress binary version: 7.5.0
Electron version: 12.0.0-beta.14
Bundled Node version:
14.15.1
Added this function that seems to be working for me for getting an element by any of the selectors in an array.
Cypress.Commands.add('getMulti', (selectors) => {
cy.document().then(($document) => {
selectors.forEach(selector => {
if($document.querySelector(selector)){
return cy.get(selector).first()
}
})
})
})
EDIT: Added extra code in the filterEvents snippet for more context.
I'm not quite understanding what's going on with my code. I'm trying to pass an array into an action function inside of my Vuex store. If I return a Promise inside of that action function, then the parameter being passed isn't of type Array and is instead an Object, which results in the reject() error that I have for the Promise.
Here's some code for context:
filterEvents({ commit }, events) {
console.log(Array.isArray(events)); //this ends up false
console.log(events);
return new Promise((resolve, reject) => {
if (!Array.isArray(events)) {
reject("Invalid argument: is not of type Array.");
}
let filtered = events.filter((event) => {
let now = new Date();
let event_stop = new Date(event.stop_time);
if (event_stop >= now || event_stop == null) {
return event;
}
});
resolve(filtered);
});
}
Here's where I call filterEvents; inside of getEvents;
getEvents({ state, commit, dispatch }, searchParams) {
.....
eventful.getEvents(searchParams).then(async (res) => {
.....
console.log(Array.isArray(res.data.events.event)); //this ends up true
console.log(res.data.events.event);
/* where I call it */
await dispatch("filterEvents", res.data.events.event).then((res) => {
.....
});
}).catch((err) => {
.....
});
}
Here's the output from the Chrome developer console. First two outputs are from getEvents and last two are from filterEvents
Would really like an explanation as to why this is the case. I'm going to bet it's something small, but it's 3 a.m. at the moment and my brain can't wrap around why it's not of type Array when passed into filterEvents.
I always try to check the length prop of the array which helps me out in such cases.
...
return new Promise((resolve, reject) => {
if (!Array.isArray(events) && !events.length) {
reject("Invalid argument: is not of type Array.");
}
.....
});
...
I finally understood what my issue was after taking another look at the object that was being logged on the console. I did not know that Vuex actions HAD to have two arguments if you want to pass in a payload into that function. For example, I initially did this
filterEvents(events) {
.....
}
but what I really needed to do was
filterEvents(context, events) {
.....
}
The context argument is the object that allows you to do things such as commit and dispatch. I usually destructure the context object (i.e. { commit, dispatch} ), so I for some reason never thought twice about it. You don't have to destructure the context object to use commit and dispatch; if you don't it would just be like
context.commit('function', payload);