Cypress Assertion that a button does not exist [duplicate] - laravel

I want to be able to click on a check box and test that an element is no longer in the DOM in Cypress. Can someone suggest how you do it?
// This is the Test when the checkbox is clicked and the element is there
cy.get('[type="checkbox"]').click();
cy.get('.check-box-sub-text').contains('Some text in this div.')
I want to do the opposite of the test above.
So when I click it again the div with the class check-box-sub-text should not be in the DOM.

Well this seems to work, so it tells me I have some more to learn about .should()
cy.get('.check-box-sub-text').should('not.exist');

You can also search for a text which is not supposed to exist:
cy.contains('test_invite_member#gmail.com').should('not.exist')
Here you have the result in Cypress: 0 matched elements
Reference: Docs - Assertions, Existence

Use .should('not.exist') to assert that an element does not exist in the DOM.
Do not use not.visible assertion. It would falsely pass in < 6.0, but properly fail now:
// for element that was removed from the DOM
// assertions below pass in < 6.0, but properly fail in 6.0+
.should('not.be.visible')
.should('not.contain', 'Text')
Migration Docs here: Migrating-to-Cypress-6-0

Cypress 6.x+ Migration
According to cypress docs on Existence
The very popular attempt which is a bit naive will work until it doesn't and then you'll have to rewrite it again... and again...
// retry until loading spinner no longer exists
cy.get('#loading').should('not.exist')
This doesn't really work for the title problem which is what most people will be looking for.
This works for the case that it is being removed. but in the case that you want it to never exist... It will retry until it goes away.
However, if you want to test that the element never exists in our case.
Yes lol. This is what you really want unless you want to just have your headache again another day.
// Goes through all the like elements, and says this object doesn't exist ever
cy.get(`img[src]`)
.then(($imageSection) => {
$imageSection.map((x, i) => { expect($imageSection[x].getAttribute('src')).to.not.equal(`${Cypress.config().baseUrl}/assets/images/imageName.jpg`) });
})

cy.get('[data-e2e="create-entity-field-relation-contact-name"]').should('not.exist');
might lead to some false results, as some error messages get hidden. It might be better to use
.should('not.visible');
in that case.

Here's what worked for me:
cy.get('[data-cy=parent]').should('not.have.descendants', 'img')
I check that some <div data-cy="parent"> has no images inside.
Regarding original question, you can set data-cy="something, i.e. child" attribute on inner nodes and use this assertion:
cy.get('[data-cy=parent]').should('not.have.descendants', '[data-cy=child]')

You can use get and contains together to differentiate HTML elements as well.
<button type='button'>Text 1</button>
<button type='button'>Text 2</button>
Let's say you have 2 buttons with different texts and you want to check if the first button doesn't exist then you can use;
cy.get('button').contains('Text 1').should('not.exist')

Could be done also using jQuery mode in cypress:
assert(Cypress.$('.check-box-sub-text').length==0)

I closed an element and checked should('not.exist') but the assertion failed as it existed in the DOM. It just that it is not visible anymore.
In such cases, should('not.visible') worked for me. I have just started using cypress. A lot to learn.

No try-catch flow in cypress
In java-selenium, we usually add the NoSuchElementException and do our cases. if UI is not displaying element for some Role based access cases.

You can also query for the matched elements inside the body or inside the element's parent container, and then do some assertions on its length:
cy.get("body").find(".check-box-sub-text").should("have.length", 0);

In case anyone comes across this, I was having the issue that neither .should('not.exist') nor .should('have.length', 0) worked - even worse: If the element I was querying was actually there right from the get-go, both asserts still returned true.
In my case this lead to the very strange situation that these three assertions, executed right after each other, were true, even though asserts 1+2 and 3 contradict each other:
cy.get('[data-cy="foobar"]').should('not.exist')
cy.get('[data-cy="foobar"]').should('have.length', 0)
cy.get('[data-cy="foobar"]').should('have.text', 'Foobar')
After extensive testing, I found out that this was simply a race condition problem. I was waiting on a backend call to finish before running the above 3 lines. Like so:
cy.wait('#someBackendCall')
cy.get('[data-cy="foobar"]').should('not.exist')
However once the backend called finished Cypress immediately ran the first two assertions and both were still true, because the DOM hadn't yet caught up rerendering based on the backend-data.
I added an explicit wait on an element that I knew was gonna be there in any case, so my code now looks something like this:
cy.wait('#someBackendCall')
cy.get('[data-cy="some-element"]').should('contain', 'I am always here after loading')
cy.get('[data-cy="foobar"]').should('not.exist')

You can also use below code
expect(opportunitynametext.include("Addon")).to.be.false
or
should('be.not.be.visible')
or
should('have.attr','minlength','2')

Voted element is correct but I highly recommend not to using anti-pattern saving you from a lot of headaches. Why? Yes, because;
Your application may use dynamic classes or ID's that change
Your selectors break from development changes to CSS styles or JS behavior
Luckily, it is possible to avoid both of these problems.
Don't target elements based on CSS attributes such as: id, class, tag
Don't target elements that may change their textContent
Add data-* attributes to make it easier to target elements
Example:
<button id="main" name="submission" role="button" data-cy="submit">Submit</button>
And if you want to be more specific and want to indentify more than one selector, it is always good to use .shouldchainer.
Example:
cy.get("ul").should(($li) => {
expect($li).to.be.visible
expect($li).to.contain("[data-cy=attribute-name]")
expect($li).to.not.contain("text or another selector")
})

If there is no element, we can use simple line like:
cy.get('[type="checkbox"]').should('not.exist')

In my case, Cypress was so fast, that simple .should('not.be.visible') was passing the test and after that, loader appears and test failed.
I've manage to success with this:
cy.get('.loader__wrapper')
.should('be.visible')
cy.get('.loader__wrapper', { timeout: 10000 })
.should('not.be.visible')
Also nice to set the timeout on 10 seconds when your application loads more than 4s.

I would use :
cy.get('.check-box-sub-text').should('not.be.visible');
This is safer than
cy.get('.check-box-sub-text').should('not.exist');
( The element can be present in the DOM but not visible with display: none or opacity: 0 )

Related

Overriding cy.get with a custom command to default to using data-cy?

I'm trying to use data-cy as much as I can in my code.
It's slightly tedious having to write cy.get('[data-cy=name]') all the time.
Is it possible to create a custom command that would by default try and find a data-cy first.
So if I wrote cy.get('name') it would try and find data-cy="name" on the page, if I used cy.get('.class') it would try and find a class with class on the page, if I used cy.get('#id') it would try and find an id with 'id` on the page?
Basically, I just want cy.get() to default to trying to find data-cy first and then work as it originally does if I pass in anything else to it.
It's a nifty idea, but you are leaning towards conditional testing if you try to do all-in-one.
I would have a custom command for data-cy and stick with cy.get() for the other selectors
Cypress.Commands.add('attr', { prevSubject: false }, (attr) => {
return cy.get(`[data-cy="${attr}"]`)
})
cy.attr('name')
Selecting UI elements based on an attribute, such as data-cy, is even described in the Cypress best practices section here.
There is also an example on how to create a custom command to have a common way for selecting the elements here.
The examples look like:
// cypress/support/commands.ts
Cypress.Commands.add('getBySel', (selector, ...args) => {
return cy.get(`[data-cy=${selector}]`, ...args)
})
Cypress.Commands.add('getBySelLike', (selector, ...args) => {
return cy.get(`[data-cy*=${selector}]`, ...args)
})
The first command looks for an exact data-cy attribute match. The second one looks for elements containing a data-cy like the provided argument.
If you add Cypress Testing Library to your project you get a bunch of commands around the data-testid attribute
ByTestId - find by data-testid attribute
getByTestId
queryByTestId
getAllByTestId
queryAllByTestId
findByTestId
findAllByTestId
See Cheatsheet for differences.
If you are tired of typing cy.get('[data-cy=name]'), then this profusion of selection methods may vex you even more.
The interesting part is the discussion about what selection methods are best for testing.
See Priority
Based on the Guiding Principles, your test should resemble how users interact with your code (component, page, etc.) as much as possible.
Interestingly, they place *ByTestId at the bottom of the list
The user cannot see (or hear) these, so this is only recommended for cases where you can't match by role or text or it doesn't make sense (e.g. the text is dynamic).

document.querySelector vs. cy.get

i just started using cypress to test a web client from Microsoft Dynamics NAV Business Central. It has dynamic id's with a very complex div structure.
My best guess on how to identify a specific input control is to use aria-label to filter it.
When I do
document.querySelector('[aria-label^="MyLabel"]')
I get the element in return. When I try
cy.get('[aria-label^="MyLabel"]')
nothing can be found. Am I expecting too much? I also tried
cy.get('input[type=text]')
and it returned nothing, although there are several text input fields on the page.
Thanks for your input/help
Update: This is the element I'm trying to find, but due to Dynamics preventing it, I can't copy the whole HTML code
<input class="cursorinherit stringcontrol-edit thm-cont-u1-font-size thm-cont-u1-font-stack thm-cont-u1-color-2--maxflat thm-cont-s1-bdrcolor--focus thm-cont-s1-outlinecolor--focus thm-cont-h1-bdrcolor--hover thm-cont-h1-bdrcolor--prev-sib-hover" type="text" spellcheck="false" maxlength="30" aria-label="Paketverfolgungsnr., (Leer)" id="C4ee" title="" autocomplete="autocomplete-off">```
The two ways of getting the elements have different usages
document.querySelector()
const inputEl = document.querySelector('[aria-label^="MyLabel"]')
console.log(inputEl)
expect(inputEl).to...
This is a synchronous call that returns a single element into the variable, which you can then use directly.
cy.get()
This is an asynchronous Cypress command that returns one or more elements which must be used inside a then() callback,
cy.get('[aria-label^="MyLabel"]').then($inputEl => {
const inputEl = $inputEl[0]
console.log(inputEl)
expect(inputEl).to...
})
This looks a lot clunkier, but is necessary when elements themselves are asynchronous (e.g populated by an API call) because the Cypress command repeats until the element arrives on the page.
In contrast document.querySelector('[aria-label^="MyLabel"]') will fail if the element is not present at the exact moment the query is run.
Multiple elements
Also note the command can get several elements at once, so you can process then together or separately
cy.get('input[type=text]').then($inputEls => {
const inputEls = [...$inputEls]
console.log(inputEls) // show all
expect(inputEls).to...
})
or
cy.get('input[type=text]').each($inputEl => {
const inputEl = $inputEl[0]
console.log(inputEl) // show individually
expect(inputEl).to...
})
After looking through the HTML DOM div by div (it felt like there were houndred at least) I realized in the middle of there Microsoft Dynamics included an iframe. This caused my requests within cypress to fail while Chrome console somehow managed to find the items regardless of the iframe.
I looked up some code to use the document of an iframe and was able to find the input element afterwards. Thank you for your help!

How To Query Through <slot> Using Cypress While Testing Web Components

After years of testing one global DOM for end-to-end testing, I'm finding it very difficult, if not impossible, to test web components that use slots. Before I explain the problem, I want to say that I cannot change the generated markup to improve things as they are.
<wc-1 attributes-etc="">
<wc-2 attributes-etc="">
<slot>
<wc-3 attributes-etc="">
<slot>
...eventually get to an input...
<input type="text" name="firstName" />
There are a buttload of nested web components from some kind of form builder, and there are also plenty of slots used. The web components have attributes but the slots never do, so I use the web component name for querying.
document.querSelector('wc-1')
.shadowRoot.querySelector('wc-2')
.shadowRoot.querySelector('slot')
// Yields <slot>...</slot>
All fine to this point and Cypress has a .shadow() command I used, but I'm testing with just devtools here to see all the properties the slot has.
document.querSelector('wc-1')
.shadowRoot.querySelector('wc-2')
.shadowRoot.querySelector('slot')
.shadowRoot
// Yields "null".
// I don't know how to get to the .lightDOM? of wc-2?
Any property I try ends up being null or having 0 elements in the returned value. Using other front-end tools and the global DOM, I can always cy.get('div[data-testid="the-nested-element-i-want"]').type('important words') in one command.
So my main question is: How do people test these things once web components start piling up? Or don't do this and just test the web components in isolation/unit tests since it's so hard to query the nested shadow DOMs?
The main goal is to eventually get to a form input to cy.get('input[name"firstName"]').type('John'). Can someone give me the chained docuement.querySelector() command to get to <wc-3> in my example?
The answer involves assignedNodes(): https://developer.mozilla.org/en-US/docs/Web/API/HTMLSlotElement/assignedNodes
The assignedNodes() property of the HTMLSlotElement interface returns a sequence of the nodes assigned to this slot...
It made no difference for me to use that vs. assignedElements(). So, all you have to do is use that method once you've queried down to the slot you need. For my example, the answer is:
const wc-3 = document.querySelector('wc-1').shadowRoot
.querySelector('wc-2').shadowRoot
.querySelector('slot').assignedNodes()
.map((el) => el.shadowRoot)[0]
And then you can keep going down the chain...I know I only have one un-named slot, so that's why I grab it from the returned .map().
Props to this Q&A for pointing me on the right direction: Web components: How to work with children?
There will be no DOM content in your <slot>, as there is no DOM content moved to slots.
lightDOM content is reflected in slots, but remains invisible! in lightDOM.
(that is why you also style slotted content in lightDOM)
From the docs:
𝘾𝙤𝙣𝙘𝙚𝙥𝙩𝙪𝙖𝙡𝙡𝙮, 𝙙𝙞𝙨𝙩𝙧𝙞𝙗𝙪𝙩𝙚𝙙 𝙣𝙤𝙙𝙚𝙨 𝙘𝙖𝙣 𝙨𝙚𝙚𝙢 𝙖 𝙗𝙞𝙩 𝙗𝙞𝙯𝙖𝙧𝙧𝙚.
𝙎𝙡𝙤𝙩𝙨 𝙙𝙤𝙣'𝙩 𝙥𝙝𝙮𝙨𝙞𝙘𝙖𝙡𝙡𝙮 𝙢𝙤𝙫𝙚 𝘿𝙊𝙈; 𝙩𝙝𝙚𝙮 𝙧𝙚𝙣𝙙𝙚𝙧 𝙞𝙩 𝙖𝙩 𝙖𝙣𝙤𝙩𝙝𝙚𝙧 𝙡𝙤𝙘𝙖𝙩𝙞𝙤𝙣 𝙞𝙣𝙨𝙞𝙙𝙚 𝙩𝙝𝙚 𝙨𝙝𝙖𝙙𝙤𝙬 𝘿𝙊𝙈.
So to test if something is "in" a slot
you need to check for slot=? attributes on lightDOM elements
and double check if that <slot name=? > actually exists in shadowDOM
Or vice versa
Or hook into the slotchange Event, but that is not Testing
pseudo code:
for the vice-versa approach; can contain errors.. its pseudo code..
function processDOMnode( node ){
if (node.shadowRoot){
// query shadowDOM
let slotnames = [...node.shadowRoot.querySelectorAll("slot")].map(s=>s.name);
// query lightDOM
slotnames.forEach( name =>{
let content = node.querySelectorAll(`[slot="${name}"]`);
console.log( "slot:" , name , "content:" , content );
});
// maybe do something with slotnames in lightDOM that do NOT exist in shadowDOM
// dive deeper
this.shadowRooot.children.forEach(shadownode => processDOMnode(shadownode));
}
}

Race condition with Capybara value set

I have faced an issue while using .set(#{value}) to fill the text field in registering form, e.g: the phone number i wanna put in is 506307 then it ended up with 063075.
The work-around i have been made is executing Javascript block like
execute_script("document.querySelector('#{selector}').value = '#{value}'")
However, using the same scripts applying for Webmobile based on React.JS, the scripts above just send the text but didn't send the onChange event, which cause another element cannot be selected/clicked -> made the test failed.
I came up with another approach is to use the send_keys #{value} to trigger the key-pressed event that would make browser think there was a key-pressed event happen for that form, but it ended up with race-condition like set(#{value}) as i mentioned.
The another work-around is using What is the best way to trigger onchange event in react js , but i tend to use the native Capybara actions before making that tricky Javascript.
So, is there any other way to interact / fill the form field which won't cause that Race condition issue ?
Thanks everybody in advance.
Note: Any "solution" suggested that is purely the use of execute_script to run some JS is a terrible idea since it completely bypasses the concept of testing what a user can do and can basically make your test worthless.
The root cause of the issue here is the JS behavior attached to the input not being able to handle the key events fast enough. The proper fix would be to fix the JS, however if that's not possible there's a few things you can try
First you can try changing the clear method being used by set
element.set('506307', clear: :backspace)
or
element.set('506307', clear: :none)
If that doesn't change anything then try clicking on the input, followed by a short sleep before setting the content
element.click
sleep 0.25
element.set('506307')
If none of those work around the issue we need to know exactly what JS behavior you have attached to the input and/or what events that JS behavior is listening to.

Acting on an element in onRender doesn't work

When I try to act on some HTML elements in the onRender method (or in a item:rendered callback), it fails.
Example:
Bars.EditGallery = Backbone.Marionette.ItemView.extend
template: 'bars/edit_gallery'
className: 'edit-gallery'
onRender: ->
# If I just write #$('select').chosen(), it doesn't work
# despite the jQuery object contains what I expect.
# To get it working, I have to write:
callback = -> #$('select').chosen()
setTimeout(callback, 0)
It's the same with others actions, like giving the focus to a field.
How do you deal with that? The trick with setTimeout works but it is not very elegant.
I've seen this happen when the templates used for rendering are loaded asynchronously. I thought I a pull request had fixed this in a recent release. What version of Marionette are you using?
But it looks like you're using JST, anyways, so that shouldn't be the problem. Is there anything else in your setup that is causing the render to happen asynchronously?
It seems likely that there is some asynchronous issue happening, though. Since using setTimeout fixes the problem, that makes me think the rendering is not completing before the onRender method is called.
Also - it can be hard tell if the jQuery selector is actually returning the object you want, right away. If you're using console.log to check the selector, this may be giving false results. console.log is itself asynchronous (in most browsers, anyways... not sure about all) which means the request to log the item gets queued up at the end of the event loop. It's likely that the DOM element is available by the time the logging happens.
FWIW: I use onRender for this exact purpose on a regular basis, and I've never had to use setTimeout to make it work. So my assumption is something funny going on with the rendering process, related to async stuff.
This problem is caused by Chosen, as mentioned in this issue.

Resources