Imagine I have the following simple html:
const inEl = document.querySelector("input")
const buttonEl = document.querySelector("button")
inEl.oninput = (() => {
buttonEl.remove()
setTimeout(() => {
document.body.appendChild(buttonEl)
}, 100)
})
.b {
background: blue;
}
<input>
<button>weee</button>
As you can see, as someone types in the input, the button is temporarily removed from the DOM. I'd like to add a cypress test that checks that the button is NOT removed from the dom (so it would need to fail for the above scenario).
It seems simple enough, but because cypress is so good at waiting for things to appear, I'm not totally sure how to write this test.
It feels like what I need is a way to throw an error if a cypress command does pass. Something like
cy.get("input").type("hello")
cy.get("button").should("not.exist") //if this passes then throw an error!
Any help on how to do this seemingly simple thing would be appreciated. Thanks!
https://docs.cypress.io/guides/core-concepts/retry-ability#Disable-retry
You can set the timeout to 0 to disable retry
so you could add
cy.get("button", { timeout: 0 }).should("not.exist")
to make sure the button does not flicker.
One possibility is to spy on the remove method
let spy;
cy.get("button").then($button => {
spy = cy.spy($button[0], 'remove')
})
cy.get("input").type("hello")
.should(() => expect(spy).to.not.have.been.called)
If you try to do visibility or existence checks you run the risk of false results, because that script will run quite quickly.
If you want to do so, you could control the clock
// This passes if the remove/append runs
cy.clock()
cy.visit(...)
cy.get("input").type("h") // one letter only
cy.tick(20) // 10ms is default delay in .type()
cy.get('button').should('not.exist') // clock is frozen part way through setTimeout
cy.tick(100)
cy.get('button').should('exist') // clock has moved past setTimeout completion
// This checks the remove/append does not run
cy.clock()
cy.visit(...)
cy.get("input").type("h") // one letter only
cy.tick(20)
cy.get('button').should('exist') // fails here if the button is removed
cy.tick(100)
cy.get('button').should('exist')
Related
I'm using Cypress 7.7.0 (also tested on 8.0.0), and I'm running into an interesting race condition. I'm testing a page where one of the first interactions that Cypress does is click a button to open a modal. To keep bundle sizes small, I split the modal into its own prefetched webpack chunk. My Cypress test starts with cy.get('#modal-button').click() but this doesn't load the modal because the modal hasn't finished downloading/loading. It does nothing instead (doesn't even throw any errors to the console). In other words, Cypress interacts with the page too quickly. This was also reproduced with manual testing (I clicked on the button super fast after page load). I have tried setting the modal to be preloaded instead, but that didn't work either.
I am able to solve the problem by introducing more delay between page load and button interaction. For example, inserting any Cypress command (even a cy.wait(0)) before I click on the button fixes the solution. Cypress, however, is known for not needing to insert these brittle solutions. Is there a good way to get around this? I'd like to keep the modal in its own chunk.
FYI: I'm using Vue as my front end library and am using a simple defineAsyncComponent(() => import(/* webpackPrefetch: true */ './my-modal.vue')) to load the modal component. I figure that this problem is general to Cypress though.
There's nothing wrong with cy.wait(0).
All you are doing is handing control from the test to the next process in the JS queue, in this case it's the app's startup script which is presumably waiting to add the click handler to the button.
I recently found that this is also needed in a React hooks app to allow the hook to complete it's process. You will likely also come across that in Vue 3, since they have introduced a hook-like feature.
If you want to empirically test that the event handler has arrived, you can use the method given here (modified for click()) - When Can The Test Start?
let appHasStarted
function spyOnAddEventListener (win) {
const addListener = win.EventTarget.prototype.addEventListener
win.EventTarget.prototype.addEventListener = function (name) {
if (name === 'click') {
appHasStarted = true
win.EventTarget.prototype.addEventListener = addListener // restore original listener
}
return addListener.apply(this, arguments)
}
}
function waitForAppStart() {
return new Cypress.Promise((resolve, reject) => {
const isReady = () => {
if (appHasStarted) {
return resolve()
}
setTimeout(isReady, 0) // recheck "appHasStarted" variable
}
isReady()
})
}
it('greets', () => {
cy.visit('app.html', {
onBeforeLoad: spyOnAddEventListener
}).then(waitForAppStart)
cy.get('#modal-button').click()
})
But note setTimeout(isReady, 0) will probably just achieve the same as cy.wait(0) in your app, i.e you don't really need to poll for the event handler, you just need the app to take a breath.
It seems like your problem is that you're already rendering a button before the code backing it is loaded. As you noticed, this isn't only an issue for fast automated bots, but even a "regular" user.
In short, the solution is to not display the button early, but show a loading dialog instead. Cypress allows waiting for a DOM element to be visible with even a timeout option. This is more robust than a brittle random wait.
I ended up going with waiting for the network to be idle, although there were several options available to me.
The cypress function I used to do this was the following which was heavily influenced by this solution for waiting on the network:
Cypress.Commands.add('waitForIdleNetwork', () => {
const idleTimesInit = 3
let idleTimes = idleTimesInit
let resourcesLengthPrevious
cy.window().then(win =>
cy.waitUntil(() => {
const resourcesLoaded = win.performance.getEntriesByType('resource')
if (resourcesLoaded.length === resourcesLengthPrevious) {
idleTimes--
} else {
idleTimes = idleTimesInit
resourcesLengthPrevious = resourcesLoaded.length
}
return !idleTimes
})
)
})
Here are the pros and cons of the solution I went with:
pros: no need to increase bundle size or modify client code when the user will likely never run into this problem
cons: technically still possible to have a race condition where the click event happens after the assets were downloaded, but before they could all execute and render their contents, but very unlikely, not as efficient as waiting on the UI itself for indication of when it is ready
This was the way I chose solve it but the following solutions would have also worked:
creating lightweight placeholder components to take the place of asychronous components while they download and having cypress wait for the actual component to render (e.g. a default modal that just has a spinner being shown while the actual modal is downloaded in the background)
pros: don't have to wait on network resources, avoids all race conditions if implemented properly
cons: have to create a component the user may never see, increases bundle size
"sleeping" an arbitrary amount (although this is brittle) with cy.wait(...)
pros: easy to implement
cons: brittle, not recommended to use this directly by Cypress, will cause linter problems if using eslint-plugin-cypress (you can disable eslint on the line that you use this on, but it "feels ugly" to me (no hate on anyone who programs that way)
I am trying to write a bookmarklet that sends me a desktop notification when CI on GitHub completes. Notification.requestPermission runs correctly, and asks me for permission, and the console.log statements run correctly, but the notification does not appear. Here is the text of the bookmarklet script:
(function() {
Notification.requestPermission().then(() => {
const search = setInterval(() => {
if (window.find("All checks have passed")) {
console.log('all checks passed');
clearTimeout(search);
new Notification('Github checks passed!');
} else {
console.log('checks pending');
}
}, 1000);
});
})();
i.e.
javascript:(function() {Notification.requestPermission().then(() => {const search = setInterval(() => {if (window.find("All checks have passed")) {console.log('all checks passed');clearTimeout(search);new Notification('Github checks passed!');} else {console.log('checks pending');}}, 1000);});})();
Is this a sandboxing thing?
I have tried with the same code you have written and tried to add few tweaks and verify the existing code.
I have seen that there not an issue with your code. I have tried to simulate the same situation with your code and it works.
In order to work on your code, I have added dummy text in the HTML body after 5 seconds of page loads, meanwhile, setTimeout function logs checks pending
After 5 seconds it a text in the body and after that it goes to search the text and the code works fine.
Here my little tweaks which might help you to identify the root cause. I guess if your code is working on these tweaks, it means that somehow in your real situation the text might not found from the HTML template.
Yes, one more thing you should keep in mind that you must allow notification when the browser asks in popup, Once you allow the show the notification, in the next attempt it will populate the notification with suggested text.
Following code, I have tried.
minify version:
javascript:(function(){Notification.requestPermission().then(()=>{let e=0;console.log("find:",window.find("All checks have passed"));const n=setInterval(()=>{if(window.find("All checks have passed"))clearTimeout(n),new Notification("Github checks passed!");else{if(5===e){const e=document.createElement("label");e.innerHTML="All checks have passed",document.body.appendChild(e)}console.log("checks pending")}e+=1},1e3)})})();
beautify version:
javascript: (function() {
Notification.requestPermission().then(() => {
let counter = 0;
console.log("find:", window.find("All checks have passed"));
const interval = setInterval(() => {
if (window.find("All checks have passed")){
clearTimeout(interval);
console.log("checks passed");
new Notification("Github checks passed!");
} else {
if (5 === counter) {
const el = document.createElement("label");
el.innerHTML = "All checks have passed";
document.body.appendChild(el);
}
console.log("checks pending");
}
counter += 1;
}, 1000)
})
})();
I have also attached the screenshots for your reference.
For your reference, this code will work in the console also it will populate the message in your console also.
Hope it might clear your idea.
This code is 100% working, so if you still face the trouble, let me know I will definitely try to help you.
As others have said, the code works as expected.
My guess is you're having problems with the setInterval function. On firefox, setInterval doesn't run when the tab isn't on focus (you also lose focus when you execute the bookmarklet from the bookmark tab).
I assume you navigated to a different tab which results in the timer stopping. Unfortunately I don't think there's an easy solution to get the result you want.
See here for reference: SetInterval not running in Firefox when lost focus on tab/window
I try to display a loading alert on Meteor with modal package during loading of data.
'change .filterPieChart': function(evt){
Modal.show('loadingModal');
/* a little bit of work */
var data = MyCollection.find().fetch(); // takes 3 or 4 seconds
/* lot of work */
Modal.hide('loadingModal');
}
Normally, the alert is displayed at the beginning of the function, and disappears at the end. But here, the alert appears only after the loading time of the MyCollection.find(), and then disappears just behind. How to display it at the beginning of the function ??
I tried to replace Modal.show with reactive variable, and the result is the same, the changing value of reactive variable is detect at the end of the function.
From what you describe, what probably happens is that the JS engine is busy doing your computation (searching through the collection), and indeed blocks the UI, whether your other reactive variable has already been detected or not.
A simple workaround would be to give some time for the UI to show your modal by delaying the collection search (or any other intensive computation), typically with a setTimeout:
Modal.show('loadingModal');
setTimeout(function () {
/* a little bit of work */
var data = MyCollection.find().fetch(); // takes 3 or 4 seconds
/* lot of work */
Modal.hide('loadingModal');
}, 500); // delay in ms
A more complex approach could be to decrease the delay to the bare minimum by using requestAnimationFrame
I think you need to use template level subscription + reactiveVar. It is more the meteor way and your code looks consistent. As i can see you do some additional work ( retrive some data ) on the change event. Make sense to actually really retrive the data on the event instead of simulation this.
Template.TemplateName.onCreated(function () {
this.subsVar = new RelativeVar();
this.autorun( () => {
let subsVar = this.subsVar.get();
this.subscribe('publicationsName', this.subsVar);
})
})
Template.TemplateName.events({
'change .filterPieChart': function(evt){
Template.instance().collectionDate.subsVar.set('value');
Modal.show('loadingModal');
MyCollection.find().fetch();
Modal.hide('loadingModal');
}
})
Please pay attention that i didn't test this code. And you need to use the es6 arrow function.
This is the first time I get my hands on with automation instruments in xcode The script works well for all button taps but the one making server connection. I don't know the reason
Here is the script I tried so far
var target = UIATarget.localTarget();
target.pushTimeout(4);
target.popTimeout();
var window=target.frontMostApp().mainWindow()
var appScroll=window.scrollViews()[0];
appScroll.logElementTree();
UIATarget.localTarget().delay(2);
appScroll.buttons()[1].tap();
The above script works up to showing the UIActivityIndicator instead of moving to next controller after success
I know There must be a very simple point I am missing. So help me out
UIAutomation attempts to make things "easy" for the developer, but in doing so it can make things very confusing. It sounds like you're getting a reference to window, waiting for a button to appear, then executing .tap() on that button.
I see that you've already considered messing with target.pushTimeout(), which is related to your issue. The timeout system lets you do something that would be impossible in any sane system: get a reference to an element before it exists. I suspect that behind-the-scenes, UIAutomation repeatedly attempts to get the reference you want -- as long as the timeout will allow.
So, in the example you've posted, it's possible for this "feature" to actually hurt you.
var window=target.frontMostApp().mainWindow()
var appScroll=window.scrollViews()[0];
UIATarget.localTarget().delay(2);
appScroll.buttons()[1].tap();
What if the view changes during the 2-second delay? Your reference to target.frontMostApp().mainWindow.scrollViews()[0] may be invalid, or it may not point to the object you think you're pointing at.
We got around this in our Illuminator framework by forgetting about the timeout system altogether, and just manually re-evaluating a given reference until it actually returns something. We called it waitForChildExistence, but the functionality is basically as follows:
var myTimeout = 3; // how long we want to wait
// this function selects an element
// relative to a parent element (target) that we will pass in
var selectorFn = function (myTarget) {
var ret = myTarget.frontMostApp().mainWindow.scrollViews()[0];
// assert that ret exists, is visible, etc
return ret;
}
// re-evaluate our selector until we get something
var element = null;
var later = get_current_time() + myTimeout;
while (element === null && get_current_time() < later) {
try {
element = selectorFn(target);
} catch (e) {
// must not have worked
}
}
// check whether element is still null
// do something with element
For cases where there is a temporary progress dialog, this code will simply wait for it to disappear before successfully returning the element you want.
I use Syn.js in my tests for my website but I have a problem when I use Syn.click and then try to check something that happens after the event like a title change or something it doesn't work.
It's like the onClick event doesn't end before it begins to check what happened after.
The onClick function is something I can't see so I can't put a custom event to make sure it's over so I need a different way to do this.
I need a way to make sure an event ended to continue to the next line in the code without changing the event itself.
I really need an idea because I don't have a clue how to continue.
It can be in JavaScript, jQuery, whatever ....
//syn.js ---- this is from the syn.js (from the internet)
"_click": function (options, element, callback, force) {
Syn.helpers.addOffset(options, element);
Syn.trigger("mousedown", options, element);
//timeout is b/c IE is stupid and won't call focus handlers
schedule(function () {
Syn.trigger("mouseup", options, element);
if (!Syn.support.mouseDownUpClicks || force) {
Syn.trigger("click", options, element);
callback(true);
} else {
//we still have to run the default (presumably)
Syn.create.click.setup('click', options, element);
Syn.defaults.click.call(element);
//must give time for callback
schedule(function () {
callback(true);
}, 1);
}
}, 1);
}
// eventually it calls element.dispatchEvent(event); in syn.js
and the user use my app like this (jasmin + syn + etc...)
// user code (sort off) : (doesnt work)
Syn.click({}, button);
expect(title).not.BeNull; // -------- here is the probleme the title need to be something but its not.
// user code (sort off) : (work)
Syn.click({}, button);
setTimeout(function() {
expect(title).not.BeNull;},1000); // ----------- I need to do this but not in here ( the user cant write this it looks bad----
The only thing I can change is the syn.js or something in between because I don't want the user to write stuff in his code that he doesn't need.
If the user writes after every Syn.*** a setTimeout it looks really ugly and I don't want that .
I also tried setTimeout in the syn.js but it still doesn't work.
Ok , I found an answer, I change the code my user give me so after every syn action everything will become the callback so only when its finished the checks will begin