I want to check the type of element found with an id. I'm using an asserton with instanceof matcher, but it's failing.
My test is as follows:
cy.get('#calculated').should(($el) => {
expect($el[0]).to.be.instanceof(HTMLDivElement)
})
My HTML is
<div id="calculated">123.40</div>
Clearly the element is a div, so what is wrong with the assertion syntax?
The instanceof element depends internally on the window that creates the element.
When you use the following, you are using HTMLDivElement from the top-level window (the Cypress runner window).
expect($el[0]).to.be.instanceof(HTMLDivElement)
But the element you are testing is created by the window inside the app iframe.
If you reference HTMLDivElement from that window, the assertion will pass
cy.window().then(win => { // app window
cy.get('#calculated').should(($el) => {
expect($el[0]).to.be.instanceof(win.HTMLDivElement) // qualify the type with
// the window reference
})
})
Related
I am working with Cypress v9.6.1 and custom elements with shadow dom. We have several custom elements which are deeply nested - I am able to query for an input that is deeply nested with several web components successfully with something like this:
// WORKS!
cy.get(‘custom-element-1’)
.shadow()
.find('custom-element-2’)
.shadow()
.find('custom-element-3’)
.shadow()
.find(`custom-element-4[id=“ce”4]`)
.shadow()
.find('input');
What I'd like to do is determine the presence of that input before moving forward in the test.
Pseudo code -
if (cy.('.deeply-nested-element').exists() ){
do.not.click(button-a)
} else {
click(button-a)
}
Following the cypress docs for conditional testing -> element presence I attempted to express the above in a conditional and use on the length attribute to determine presence
//DOES NOT WORK
cy.get(‘custom-element-1').then(($ce) => {
if($ce.shadow()
.find('custom-element-2’)
.shadow()
.find('custom-element-3’)
.shadow()
.find(`custom-element-4[id=“ce”4]`)
.shadow()
.find('input')) // then do something
}
This gave me an error. Property 'shadow' does not exist on type 'JQuery<HTMLElement>'. Did you mean 'show'?ts(2551)
Ignoring the ts error resulted in cypress test failing with:
$ce.find(...).shadow is not a function
I switched it up a little by chaining off of shadow() with the same result
//ALSO DOES NOT WORK
cy.get(‘custom-element-1').shadow().then(($ceshadow) => {
$ce
.find('custom-element-2’)
.shadow()
.find('custom-element-3’)
.shadow()
.find(`custom-element-4[id=“ce”4]`)
.shadow()
.find('input');
}
And for this one:
$ce.find(...).shadow is not a function
It looks to me like the promise off of the get method does not pass to the callback an element with a shadow dom (JQuery). The small problem I'm trying to figure out is a workaround to that. The larger problem is how to set up a conditional that is determined by the presence of an element that is deeply nested within custom element shadow doms. Any advice would be much appreciated.
The .shadow() command is a Cypress command, but $ce is a jQuery object that can't call Cypress commands directly.
$ce.find(...).shadow is not a function occurs because both jQuery and Cypress have .find(), but only Cypress has .shadow().
By making includeShadowDom:true global either in config or the test options, Cypress commands can be used without needing to chain .shadow() at every step.
If it's custom-element-1 that is conditional, this should work
it('tests deeply-nested shadow elements', {includeShadowDom:true}, () => {
cy.get('body').then($body => {
const ce1 = $body.find('custom-element-1');
if (ce1.length) {
cy.wrap(ce1) // wrap jQuery element into Cypress result
// so that includeShadowDom:true is effective
.find('custom-element-2')
.find('custom-element-3')
.find('custom-element-4[id="ce4"]')
.find('input'))
}
})
})
I just started playing with cypress and I am trying to write down some tests in my sandbox application. In my first test user should click on a 'button' to make the toolbar to appear, then click on a button to activate the feature, then click a couple of times on a leaflet map to draw a line.
As you can see: click on 'Tools', then click on 'draw route' button and then click on map to draw.
This dummy app is wrapped inside a web component, here is the code:
And here is my test code:
describe('Draw geometries on map', ()=>{
beforeEach(() => {
cy.visit('http://192.168.49.2:30000/scouter/');
})
...
it('can draw after clicking draw button', ()=>{
cy.get('scouter-web').shadow().find('.scouter-tools-main-button').click()
cy.get('scouter-web').shadow().find('draw route').click()
console.log('new cy')
cy.get('scouter-web').shadow().get('scouter-web').shadow().find('#map')
.click(400, 400)
.click(400, 600)
.click(500, 600)
})
});
Problem is I, after clicking 'Tools', I can't 'find' the 'draw route' button. What am I missing? The whole stuff can be found here, subproject is scouter-web.
Instead of using the shadow() repeatedly, you can mention includeShadowDom: true once in your cypress.json file.
With find you can just use selector but I think you are using text. If you just want to use text, you can use contains.
cy.contains('draw route').click()
And if your application is throwing Uncaught Exceptions you can add to your cypress/support/index.js to globally turn off all uncaught exception handling. But a fair bit of warning, do this only when you are sure that the exceptions generated can be ignored.
Cypress.on('uncaught:exception', (err, runnable) => {
// returning false here prevents Cypress from
// failing the test
return false
})
I am not allowed to answer questions yet, but I feel that since this issue has taken me some days to resolve, I should post the solution the only way I can - as a question.
If you have a better solution then please include that in your reply.
Until an element is rendered it is not in the DOM so if you add an event listener to the element in your code you will get an error (element value null).
But you can add a listener to the root element, which is always there. WHen the event is triggered you can then retrieve the className and ID of the element involved in your event-handler.
Code:
var rootElement = document.getElementById('root');
console.log(rootElement);
rootElement.addEventListener('click', rootElementClicked);
console.log('event listener added to root element');
function rootElementClicked(event) {
event.preventDefault();
const { name, value } = event.target;
console.log("Root element clicked with [" + event.target.className, event.target.id);
}
/code
So, the event-listener is app-wide, so a click anywhere will call the event-handler function. Then in the code for that function, the element class and ID will tell you what was clicked.
Note the event.preventDefault(); line - it prevents a refresh of the web page, otherwise the target class & ID are returned as "undefined"
In React you shouldn't need to addEventListener manually since you can use onClick. However if you would like to manually attach click handler on an HTML element in React you can use React Ref. If you pass a ref object to React with <div ref={myRef} /> React will set its .current property to the corresponding DOM node whenever that node changes.
Example: https://codesandbox.io/s/react-hooks-useref-xfvlb
const App = () => {
const elementRef = useRef();
useEffect(() => {
elementRef.current.addEventListener('click', handleOnClick);
},[elementRef])
const handleOnClick = () => {
alert("click")
}
return (
<div className="App">
<div ref={elementRef}>
click on me
</div>
</div>
);
}
I've a unpredictable list of rows to delete
I simply want to click each .fa-times icon
The problem is that, after each click, the vue.js app re-render the remaining rows.
I also tried to use .each, but in this cas I got an error because element (the parent element, I think) has been detached from DOM; cypress.io suggest to use a guard to prevent this error but I've no idea of what does it mean
How to
- get a list of icons
- click on first
- survive at app rerender
- click on next
- survive at app rerender
... etch...
?
Before showing one possible solution, I'd like to preface with a recommendation that tests should be predictable. You should create a defined number of items every time so that you don't have to do hacks like these.
You can also read more on conditional testing, here: https://docs.cypress.io/guides/core-concepts/conditional-testing.html#Definition
That being said, maybe you have a valid use case (some fuzz testing perhaps?), so let's go.
What I'm doing in the following example is (1) set up a rendering/removing behavior that does what you describe happens in your app. The actual solution (2) is this: find out how many items you need to remove by querying the DOM and checking the length, and then enqueue that same number of cypress commands that query the DOM every time so that you get a fresh reference to an element.
Caveat: After each remove, I'm waiting for the element (its remove button to be precise) to not exist in DOM before continuing. If your app re-renders the rest of the items separately, after the target item is removed from DOM, you'll need to assert on something else --- such as that a different item (not the one being removed) is removed (detached) from DOM.
describe('test', () => {
it('test', () => {
// -------------------------------------------------------------------------
// (1) Mock rendering/removing logic, just for the purpose of this
// demonstration.
// -------------------------------------------------------------------------
cy.window().then( win => {
let items = ['one', 'two', 'three'];
win.remove = item => {
items = items.filter( _item => _item !== item );
setTimeout(() => {
render();
}, 100 )
};
function render () {
win.document.body.innerHTML = items.map( item => {
return `
<div class="item">
${item}
<button class="remove" onclick="remove('${item}')">Remove</button>
</div>
`;
}).join('');
}
render();
});
// -------------------------------------------------------------------------
// (2) The actual solution
// -------------------------------------------------------------------------
cy.get('.item').then( $elems => {
// using Lodash to invoke the callback N times
Cypress._.times($elems.length, () => {
cy.get('.item:first').find('.remove').click()
// ensure we wait for the element to be actually removed from DOM
// before continuing
.should('not.exist');
});
});
});
});
I have the following script:
$("#border-radius").click(function(){
var value = $("#border-radius").attr("value");
$("div.editable").click(function () {
mySQLinsert(value, '2', this.id)
$(this).css({
"-webkit-border-radius": value
});
});
});
Basically, the "#border-radius" ID is to a text input. You type in a value, click the textbox (can't figure out a way around this) and then click the div with the class "editable" and it will apply the style "-webkit-border-radius" (I know its not cross-browser), to that DIV.
The mySQLinsert function uses Ajax to send the value of the mySQLinsert function to a mySQL database using a php page (nothing fancy).
Issue: When I click the child div (div inside of a div) it also applies this value to the parent object, and runs the mySQLinsert function and stored it in the database
I need to be able to select both the parent, and the child individually depending on which one is clicked.
You can use stopPropagation() to prevent the click bubbling up to its parent element.
$("#border-radius").click(function(){
var value = $("#border-radius").attr("value");
$("div.editable").click(function (e) {
// Note the parameter e passed in the function above
e.stopPropagation();
mySQLinsert(value, '2', this.id)
$(this).css({"-webkit-border-radius": value});
});
});