Issue:
We are triggering a snackbar from a button within a dialog and the snackbar is not being read. It reads fine when triggered from a button outside the dialog.
Desired behavior:
When the user clicks the "OPEN SNACKBAR" button from within the dialog the snackbar is read by the screen reader.
Codesandbox:
https://codesandbox.io/s/dialog-snackbar-testing-sje9p?file=/demo.js
Steps to reproduce issue:
Note: If you would like to see how the snackbar should be read by the screen reader Click the first "OPEN SNACKBAR" button presented.
Open NVDA screen reader or JAWS screen reader
Go to https://codesandbox.io/s/dialog-snackbar-testing-sje9p?file=/demo.js
Click "OPEN DIALOG" button
Click "OPEN SNACKBAR" button
Testing details:
Button trigger: JAWS + Chrome = PASS
Button trigger: JAWS + Firefox = PASS
Button trigger: JAWS + Edge = PASS
Button trigger: NVDA + Chrome = PASS
Button trigger: NVDA + Firefox = PASS
Button trigger: NVDA + Edge = PASS
Dialog button trigger: JAWS + Chrome = FAIL
Dialog button trigger: JAWS + Firefox = FAIL
Dialog button trigger: JAWS + Edge = FAIL
Dialog button trigger: NVDA + Chrome = FAIL
Dialog button trigger: NVDA + Firefox = FAIL
Dialog button trigger: NVDA + Edge = FAIL
I think you are getting lucky that your first example works. It looks like you are not using aria-live correctly in either test case. aria-live is meant to exist on the page and not be added dynamically itself.
That is, for aria-live to work correctly all the time no matter which browser or screen reader or platform, your page should look something like this:
Before
<div>stuff</div>
<div aria-live="polite"> <!-- intentionally empty --> </div>
<div>stuff</div>
And when you add the snackbar message, it would be injected into the empty <div> and would be announced properly.
After
<div>stuff</div>
<div aria-live="polite"> snackbar stuff </div>
<div>stuff</div>
What doesn't work reliably is adding aria-live elements dynamically. The following may or may not work on some browser + screen reader combinations:
Before
<div>stuff</div>
<div>stuff</div>
After
<div>stuff</div>
<div aria-live="polite"> snackbar stuff </div>
<div>stuff</div>
Note that my examples use aria-live directly. In your sample, your snackbar is using role="alert" which gives you an implicit aria-live="assertive". For the purposes of having the text announced, it doesn't matter if you have "polite" or "assertive". The main issue is adding a role="alert" (ie, aria-live) dynamically. It generally doesn't work.
The fact that you hear it with the button example but not the dialog example is weird but the first thing you have to eliminate is the dynamic aria-live. If you do that and it still doesn't work in both cases, I can take another look.
The issue is multiple live regions on the page.
When you add your first snackbar you expose role="alert" (which is the same as aria-live="assertive" effectively).
This should work pretty well across all browsers (a single alert).
However, multiple aria-live regions tend to fail in numerous screen readers, especially when dynamically added and removed.
The best way to deal with this is to move all screen reader alerts into a single aria-live region and set up a message queue to handle alerts at the application level, this way you only ever have one aria-live region (2 if you need a polite and an assertive option) and won't run into as many issues.
A quick Example / the concept
HTML
<!-- You can add and remove these at will, this is the dynamically added snackbar -->
<div class="snack"></div>
<!-- This will **always** exist on the page and we will update the contents from our message queue -->
<div id="alertAnnouncer" aria-live="assertive">
</div>
Pseudo code
// Adds the snack bar with the specified text. This has nothing to do with the screen reader message queue, it is just to represent your current function to add a snack bar.
addSnackBar("Alert Text");
// this is how we add messages to the queue.
addScreenReaderAlert("Alert Text");
// we hold our messages here for processing.
var messageQueue = [];
function addScreenReaderAlert(textToAnnounce){
messageQueue.push(textToAnnounce);
//CODE TO ADD: start the interval to process the queue if it is not running, if no update has happened in the last x seconds process the message immediately and set the last time processed or something similar.
}
function processMessageQueue(){
if(messageQueue.length > 0){
//CODE TO ADD: get oldest message (you would use slice etc. and remove it entirely from the queue)
// ultimately we want this
let oldestMessage = messageQueue[0];
// we change the inner HTML of the aria-live region so the oldest message is announced.
return document.querySelector('#alertAnnouncer').innerHTML = oldestMessage;
}else{
//CODE TO ADD: clear the interval
}
//NOTE: to save CPU cycles I would stop the interval once the message queue reaches 0 length and then restart the message queue interval when you fire the addScreenReaderAlert() function.
}
// this is purely to show there is an interval on the queue, you would set and unset this as described above.
Window.setInterval(processMessageQueue, 1000);
// NOTE: The interval delay should be set to a value large enough to allow a screen reader to announce the messages. So for a simple snackbar this can be shorter, but if you want to announce longer messages you need to set this higher. Note that the interval is purely for spreading out multiple messages added in quick succession so you don't overwrite them too quickly.
Explanation of code
That all looks complicated and messy, my apologies!
But essentially we want a way of:
adding a message to a queue
processing that queue with a gap between each update to the aria-live region.
Ideally we process items immediately if there hasn't been a message in the last x seconds.
The gap between messages is to ensure you do not overwrite the message currently being spoken.
You can set up aria-live regions so that you can continue adding messages to them (appending the message rather than replacing), but this is not always stable in all screen readers (older NVDA, ORCA etc. don't always read new items after a certain length). This workaround is pretty robust once you fine tune the message timings etc.
The message queue, although a little bit of work to set up in the first place, is also a super easy way to manage all alerts etc. across your whole application without worrying about multiple alerts overlapping etc.
If you need more explanation (as I am not sure if I "waffled" on in this explanation 🤣) just let me know.
Related
Ok, I know that title could use some work, but I'm not sure how else to put it.
Here's the setup.
I have a (potentially) massive table that gets generated via an ng-repeat. All the rows need to be editable but, when the dataset is so large, all those bindings slow things to a crawl. I could literally be waiting upwards of 20 seconds for large sets to load!
We noticed that dumping the data in a read-only state significantly decreased the load time. So, we came up with the idea of loading it read-only, but, when the table row was clicked, it became editable. This is accomplished like so. I have two cells output. editableRow is initially false. When the row is clicked, editableRow becomes true. The idea being that, when editableRow becomes true, I see the other cell.
(proprietary code obfuscated)
<TABLE-CELL class="value-col" ng-if="readtime.editableRow === true">
<input type="text"
name="readingTime"
ng-model="<data model>"
ng-disabled="<param>"
ng-change="<function>"
ng-class="<classes>"
/>
</TABLE-CELL>
<TABLE-CELL class="value-col" ng-if="readtime.editableRow === false">
<input type="text"
placeholder="{{<data model>}}"
ng-class="<classes>"
/>
</TABLE-CELL>
The problem is, on the click, for a tiny fraction of a second both cells are visible. It really is only visible on the first click. Subsequent clicks still do it, but it goes so fast that the human eye can't catch it. I know it's there since I slowed everything down with a breakpoint on the mouse click. This also revealed that this happens as the value turns true - turning on the first cell, but the second one doesn't disappear in the same moment. So, it causes a "flicker" of sorts. This seems to happen outside my actual code, inside the jQuery, so I'm not sure how to short circuit it.
I've tried playing with using ng-show/hide instead, which worked a little bit, but also totally negated the time-saving aspect, since it actually renders everything, and it took a long time. I've also tried ng-cloak with no effect whatsoever.
The breakpoint that it keeps stopping on (when I told it to stop on event listeners to do with the mouse click) is the following code in jquery.js:
if ( !(eventHandle = elemData.handle) ) {
eventHandle = elemData.handle = function( e ) {
// Discard the second event of a jQuery.event.trigger() and
// when an event is called after a page has unloaded
return typeof jQuery !== strundefined && jQuery.event.triggered !== e.type ?
jQuery.event.dispatch.apply( elem, arguments ) : undefined;
};
}
It hits that line about 4 times, and, on the last one, both cells are visible. Then, the second one disappears.
I'm out of ideas and would appreciate any thoughts on this.
I finally found an answer that works!
On this page: disable nganimate for some elements the answer right BELOW the accepted answer is what finally worked!
To disable ng-animate for certain elements, using a CSS class, which follows Angular animate paradigm, you can configure ng-animate to test the class using regex.
Config
var myApp = angular.module("MyApp", ["ngAnimate"]);
myApp.config(function($animateProvider) {
$animateProvider.classNameFilter(/^(?:(?!ng-animate-disabled).)*$/);
})
Usage
Simply add the ng-animate-disabled class to any elements you want to be ignored by ng-animate.
strange behavior with fuse cursor. I've set fuse false for cursor both mainscene and a-cursor, but fuse still appears on hover.
This problems is apeearing when testing on ios, android. From desktop browser working fine.
<a-camera id="default_angle" camera position="0 0 0" look-controls wasd-controls>
<a-cursor>
<!-- by default click should be not expected on hover object (fuse) -->
</a-cursor>
</a-camera>
...
<a-box id="motor" color="red" position="0 0 -5"></a-box>
<!-- on hover to this object from mobile fuse should not appear -->
...
var motor = document.querySelector('#motor');
motor.addEventListener('click', function (evt) {
alert('clicked');
});
Please checkout this codepen
https://codepen.io/sevenspring/pen/XWWzNvK
Here may be thought, that clicks produced by touch (tap on mobile screen) while cursor is hovered on object.
but you may check this example with gyro.
https://parent.glitch.me
in vr mode we can hover cursor on button "play/pause" by gyroscop moving, it still will produce clicks.
https://glitch.com/edit/#!/join/d1afa869-dea2-47ec-9904-e851e451d832
Answer from Dan S. is correct.
When we use without attribute "cursor" , fusing event is not fired, as it is expected by default
There are actually a number of issues here.
One issue is that the CodePen demo is using <a-cursor fuse="false" cursor></a-cursor>. This is essentially two cursors as you're adding a second one as a component via cursor that hasn't been configured.
Another issue is that cursor fuses by default on mobile if not explicitly set to cursor="fuse: false;"
Another issue is that you're listening for the click event, which is capturing the click when you mouseup after hovering over the entity.
I believe fuse was designed to allow for click without clicking, but while also allowing users to click if they have the ability.
If you want to test when something is fusing exclusively, you may want to try listening for the fusing event rather than click.
<a-cursor fuse="false"><a-cursor>
Or
<a-entity cursor="fuse: false;">
<!-- you will need to provide your own cursor geometry -->
<a-entity>
Then listen for fusing:
this.el.addEventListener('fusing', function (e) {
console.log(e);
});
I hope this helps.
I'm trying to click the continue button after filling in the fields on this webpage. But an exception is thrown saying element is not visible even though I maximize the screen and you can clearly see the button.
I have tried the following even with waiting 10 seconds for the page:
driver.findElement(By.xpath("//*[#id=\"submitButton\"]/span")).click();
driver.findElement(By.cssSelector("#submitButton > span")).click();
driver.findElement(By.partialLinkText("Continue")).click();
driver.findElement(By.className("caret_rebrand")).click();
driver.findElement(By.name("submitButton")).click();
driver.findElement(By.xpath("//span[contains(#class,'red') and contains(text(), 'Continue')]")).click();
Here is the part of the html I am trying to access:
<button style="padding-left: 0px;" type=button" "id=submitButton" class = "nbutton" title = "Continue" onclick=_hblicnk('continue,'continue'); goFeaturePage('true');">
<span style = "padding-right: 12px;" class="red"
"Continue"
<img class="caret_rebrand">
</span>
I expect the continue button to be found and clicked. attached is the picture of the webpage
UPDATE: 8-3-19: I've tested the following pieces of code and it is able to find the element in all cases. But when adding the .click() function to any one of them, it causes a no such element exception.
driver.findElement(By.name("submitButton")).click();
driver.findElement(By.id("submitButton")).click();
driver.findElement(By.cssSelector("#submitButton")).click();
driver.findElement(By.xpath("//*[#id=\"submitButton\"]")).click();
My expectation is that you need to click the <button> element as this <span> might be not clickable at all. You can use i.e. title attribute to uniquely identify the element on the page
It's better to use Explicit Wait to ensure that the button is present and clickable as it might be the case it's not available in DOM right away. Check out How to use Selenium to test web applications using AJAX technology article for more details
Assuming all above I believe you should be able to use below code for clicking the button:
continue_button = WebDriverWait(driver, 10).until(EC.element_to_be_clickable((By.XPATH, "//button[#title='Continue']")))
continue_button.click()
I am trying to click a button that is buried in div classes in the code via protractor.
I am pioneering a protractor project for my work and have reached a point where I no longer know what to do. I have a button that is buried in div classes and is not allowing me to click. I have tried using mouseMove to get over to the coordinates of the button, I have tried using the className of the specific button, etc. The button does not have an id. The id is not the issue as I have tried clicking a different button, equally buried in divs, by it's id. I need to know how to get through the layers of divs in order to click the button because the rest of the tests will be dependent on it.
APPLICATION CODE:
::before
<dashboard-label>
<div class="att-topic-analysis-tabs">
<div class="att-button-group">
<button class="btn btn-default btn-lg att-close-topic ng-scope"
role="presentation" tabindex="-1"
ng-click="removeTopic(currentTopic.id)" translate>
Close Topic
</button>
</div>
</div>
PROTRACTOR TEST:
it('Closes Topic Successfully', function(){
//opens the first available topic
openTopic.click();
//checks that the URL contains 'topics' after 5 seconds
browser.wait(proExpect.urlContains('topics'), 5000);
var closeTopic = element(by.className('att-close-topic'));
//browser.wait(proExpect.elementToBeClickable(closeTopicButton), 5000);
console.log(closeTopic);
closeTopic.click();
browser.wait(proExpect.urlContains('home'), 5000);
});
As you can see, the Close Topic button is kind of buried in div classes and the standard click isn't working. Any info would be greatly appreciated
If the closeTopic locator is finding the element, but failing to click it, check to make sure there's only one matching element in the DOM, and that it's visible. My favorite way to check the DOM is just ctrl-F in Chrome inspector and paste the exact CSS that the test is using (.att-close-topic). And to check that what it's getting is visible, use
console.log(closeTopic.isDisplayed());
This can be a big gotcha in protractor, because it doesn't fail (only warns) when there are multiple matches on the page, and it defaults to the first match rather than the first visible match, which drives me nuts, because it's very rare that you want to do anything with a non-visible element on the page.
This will be partly opinion, but just to add a layer to the conversation...
Sometimes the solution to locating a troublesome element on the page is to go back to the developers and make the page more testable. I've seen testers spend hours or days crafting brilliant workarounds to access a stubborn element, and the end result was a fragile, complicated end-to-end test (and aren't they fragile enough already?).
Sometimes a 5-minute conversation with a developer can result in a quick change in the production code (e.g. add a unique ID) that avoids all that effort and yields a much better result, more stable, more simple. But this requires open conversation between the dev and test team, and a culture that values testing as a primary activity enough to make those testability changes to production code that is otherwise working just fine.
This is what you want to read to help you debug why your test doesn't work.
Also, you might want to start adopting await/async since the control flow will go away in the future.
http://www.protractortest.org/#/debugging
try this
var closebutton=element(by.css("[ng-click="removeTopic(currentTopic.id)"]"),
EC = protractor.ExpectedConditions;
Waits for the element to be clickable.checks for display and enable state of button
browser.wait(EC.elementToBeClickable(closebutton), 10000);
now use : closebutton.click();
My HTML code for a NEXT button which I am trying to click is:
<input type="submit" name="ctl00$ctl00$MainContent$ChildContent1$btnStep2_Submit" value="Next" onclick="waitBtn(event,'df_step2');
OverlayProgressPanel_NoSub('ctl00_ctl00_MainContent_ChildContent1_fsLoanData','ctl00_ctl00_upProgress');
" id="ctl00_ctl00_MainContent_ChildContent1_btnStep2_Submit" class="btn" onmouseover="hov(this,'btn btnhov');" onmouseout="hov(this,'btn')" />
Am using the below code to click that next button:
xpath = ".//*[#id='ctl00_ctl00_MainContent_ChildContent1_btnStep2_Submit' and #type='submit']"; // Next Button
Function_Classes.field_click(driver,xpath); // Click on Next button
driver.manage().timeouts().implicitlyWait(20,TimeUnit.SECONDS);
But,it looks like the button is getting clicked but not going to the next page where there is one more NEXT button
I am getting below exception :
Unable to locate element: {"method":"xpath","selector":".//*[#id='ctl00_ctl00_MainContent_ChildContent1_btnStep3_Submit' and #type='submit']"}
ctl00_ctl00_MainContent_ChildContent1_btnStep3_Submit = this is the button on the next page and since I am not getting the next page on clicking the first NEXT button ,I would think that might be the reason..
Can you please help me here?
IDs by HTML standards should be unique on the page so you shouldn't need to specify anything other than the ID in the locator. That will simplify your locators. So, in this case you can use By.id().
You've added an implicit wait in the middle of your script. That leads me to believe that you don't understand how implicit waits work. An implicit wait is set once for the driver instance and lasts the life of the driver. Generally you would put this statement near the top of your script. I would advise against using implicit waits and instead prefer explicit waits (WebDriverWait). I've rewritten the code. I think the below should work.
By next2Locator = By.id("ctl00_ctl00_MainContent_ChildContent1_btnStep2_Submit");
By next3Locator = By.id("ctl00_ctl00_MainContent_ChildContent1_btnStep3_Submit");
driver.findElement(next2Locator).click();
new WebDriverWait(driver, 10).until(ExpectedConditions.visibilityOfElementLocated(next3Locator)).click();
Suggestion: Don't overcomplicate your code by adding wrapper functions around .click(), etc. There's really no need for it. It makes your code harder to read and introduces unnecessary complexity. Keep it as simple as you can.
There is a problem inside your html, Because Threre are few functions are available inside your html, and you didn't call all functions properly. so your button click event was not fire. functions name like this waitBtn, hov.
See image for more details.