kuceb / cypress-plugin-tab Goto Github PK
View Code? Open in Web Editor NEWA cypress plugin to add a tab command
License: MIT License
A cypress plugin to add a tab command
License: MIT License
Hi!
I created few tests for @valu/focus-trap using this. Unfortunately it seems that it does not work quite right if tests are executed with window focus.
Steps to reproduce:
git clone [email protected]:valu-digital/focus-trap.git
cd focus-trap/
git checkout 82863d8024d1d061de8d5a0a200babb8293d8739
npm ci
Start server in another shell
npm run examples-dev
Open Cypress
npm run cypress
Hit "Run all specs" and observe how bunch of the tests fail.
Next open cypress/integration/focus-trap.spec.ts
and edit some test name and hit save, live reload should trigger test rerunning and now observe how all tests pass.
I'm sometimes able to make tests pass when pressing this re-run button
but not always. Not sure why. The live reload is the only way I'm able make them pass consistently.
Any ideas what might be going on?
@valu/focus-trap
works by tapping into the focusin
event, calls stopImmediatePropagation()
on it and programmatically focuses elements it wants with .focus()
. The most relevant code here is here
Anyway thank you for creating this! This is the only tool I've been able to create any tests for library like this. Cheers!
This may or may not be an actual issue. I'm not sure the intent.
I'm creating a modal with a focus trap. The way the focus trap works is it will cancel the keydown event if focus will travel outside the trap. cy.tab
will return an empty subject in this case even if the focus lands somewhere:
onCancel
is defined as _.noop
here:
https://github.com/Bkucera/cypress-plugin-tab/blob/ebce91c3d6ff8399e11f1f26a559588e4d1779ce/src/index.js#L67
From my experiments, I could change the _.noop
to be a function that returns the activeElement
of the document. This works:
const simulatedCancel = () => {
// Perhaps a focus trap cancelled? Return the activeElement
return cy.now('focus', cy.$$(doc.activeElement));
};
return Promise.try(() => {
return keydown(activeElement, options, simulatedDefault, simulatedCancel);
}).finally(() => {
keyup(activeElement, options);
});
I see that newElm.focus()
was actually commented out... It made me think, why is tabsequence
even needed? Can we just always send the correct key events and just always return the new doc.activeElement
whatever that might be? This would allow support for tabIndex=-1
which is currently not supported (because the element doesn't exist in the tabsequence), but works in the browser regardless.
How can I use it with TS?
I see that the types are been exported, but I don't know how to add it in cypress to detect in the lint.
Thanks!
Hello ๐ I'm writing on behalf of the Cypress DX team. We wanted to notify you that some changes may need to be made to this plugin to ensure that it works in Cypress 10.
For more information, here is our official plugin update guide.
Would love the ability to say "hit tab until you match" to ensure things are in a flow.
cy.visit(url)
.get('#carousel-xyz')
.find('[data-item=0]')
.focus()
.tabUntil('.right-arrow');
Thanks!
I tried the cypress-plugin-tab, I have some differences in order at manual testing, if I have divs in body, which have different tabindices, e.g. this one:
<html>
<head>
</head>
<body>
<div tabindex="10">1</div>
<div tabindex="5">2</div>
<div tabindex="3">3</div>
</body>
</html>
When using the plugin, the first cy.get("body").tab() selects the div with "10" tabindex.
But at manual testing the "3" tabindex won.
Is it a bug of the plugin, or I did something wrong? How can I fix that?
It seems to me alphabetical order instead of numerical order.
Sadly I don't have a super clean example of this in a Plunkr or similar but I'm having an issue tabbing out of elements that are focusable through an included [tabindex="-1"]
.
On navigation to some of our pages, we move the default focus to the titles as a way to skip our logo by default via a -1 tabindex and a focus shift on initialize. Cypress correctly passes these elements as having focus (which is verified via manual testing as well) but trying to tab()
out of those elements results in an error from the plugin.
cy.get('#passwordResetTitle')
.should('have.focus')
.tab();
Let me know what other info would be helpful here! Or if I'm using the plugin incorrectly to test these interactions, also let me know that.
Edit:
Using cy.focused().tab();
also results in the same behavior. Seems like Cypress sees the elements as focusable, but the plugin differs from what it considers focusable.
I ran into an edge case where cy.tab()
happens too fast. You can actually witness this while watching the GUI. If you cy.tab().tab()
, you can't see focus changing. If I added a cy.wait()
, I could see focus changing.
I'm building an accessible modal for github.com/Workday/canvas-kit: Workday/canvas-kit#59. Our Accessibility Specialists want the header to receive focus when the modal opens if there is no close icon (a config option for modals). That header should not be focusable again (removes the tabIndex on blur). cy.tab
executes so fast that the DOM doesn't have time to reflect this change (since there is no wait between tabs). The blur
fires and the tabIndex attribute is removed, but the DOM isn't updated by the time the next Cypress command runs.
Adding a wait for a single frame (using requestAnimationFrame
) solves this issue where focus
or blur
events are allowed to execute and the DOM has been updated before continuing. This change also allows the focus ring to visibly change while the test is running.
Some other commands either wait 50ms or use https://github.com/cypress-io/cypress/blob/develop/packages/driver/src/cy/actionability.coffee
I can override tab
in my support file to force a wait to fix this, but perhaps this will cause issues for other people.
It seems that some recent browser changes have started causing issues in platform.js
which is a dependency of a11y.js
used here. More details can be seen on the platform.js
issue here: bestiejs/platform.js#196. It seems a11y.js
hasn't been touched for 5 years and platform.js
for 2. So I doubt they will get patched anytime soon. Given this package only uses 1 method from a11y.js
it seems like we could probably just include that functionality internally, and removed the need for any dependencies?
When working with Ionic apps that heavily use Shadow DOM, it looks like we'll need to be able to pass strategy: 'strict'
to this plugin.
I could try submitting a PR. A new option strategy
?
cy.get('body').tab({ strategy: 'strict' });
For my app, I think I'd then use overwrite
to always set this as my default.
import 'cypress-plugin-tab';
Cypress.commands.overwrite('tab', (originalFn, options) => {
// allow overriding `strategy` per query, but default to 'strict' if undefined
return originalFn({ strategy: 'strict', ...options });
});
Thoughts?
The typescript type definition for the index.d.ts is not specified in package.json as "types": "src/index.d.ts"
and so is inaccessible to typescript. The fix is to release the exact same code with a "types"
field in package.json
I am using this plugin and it requires me to call .tab() twice before an event is registered/fired
examples:
cy.get('body').tab(); // does not register/fire event
cy.get('body').tab().tab(); // fires an event
is this a known issue ? as i am trying to replicate a users path through a system and need event to fire on one tab().
Cypress has updated to version 4.x.x, which will require this plugin to update its dependencies in order to maintain compatibility with the current version. Thank you!
Hi there, thanks for this plugin! I'm trying to get it working for the first time and having a little trouble. I've installed the plugin, added require('cypress-plugin-tab')
to cypress/support/index.js, but when I go to use the tab()
method I receive the following error.
Property 'tab' does not exist on type 'Chainable<JQuery<HTMLBodyElement>>'.ts(2339)
I'm using this withing a lerna monorepo, in case it helps diagnose. I have a branch you can checkout to run it locally if you like, it lives here https://github.com/seanforyou23/patternfly-react/tree/cypress-tab-support. You can run this locally with the following:
git clone [email protected]:seanforyou23/patternfly-react.git && cd patternfly-react/
yarn && yarn build:pf4 && yarn start:demo-app
# then, while "yarn start:demo-app" is running, in another console run the following
yarn start:cypress
I'm trying to use it for a simple case with button, you can see where I attempt here seanforyou23/patternfly-react@af77075#diff-ad8dcb99e87958a35e5815311202b43dR21
Please let me know if you spot anything I'm doing wrong, looking forward to using this plugin. Thanks!!!
Hello,
I have a question if your plugin can help me to do the following
Let's say I have a page with an icon when I click on it, it generates a PDF report in a different tab. Then I want to navigate to that tab and run cy.screenshot(). There will be no web element available on that tab because It's just an Adobe PDF browser container. Please, let me know
Thanks
Jeff
Running npm audit results in the following dependency vulnerability:
Is anyone able to give an update on whether this is being looked into? This is non-breaking for me, and I understand that 3rd party packages might not have releases to remove such warnings, but this is creating a lot of noise for my project.
Any insight would be much appreciated!
I noticed while using this plugin that a Cypress Command log only happened in certain instances. It only creates Command Log entries when the tab event isn't cancelled. In my case, I'm using a focus trap and the library often cancels the event.
Logging happens implicitly here:https://github.com/Bkucera/cypress-plugin-tab/blob/8e74c21b084fe83fc8c036a879694be0f83481b5/src/index.js#L60 which runs https://github.com/cypress-io/cypress/blob/6ed8d31cf045acd486757474934ef84f5f96cb74/packages/driver/src/cy/commands/actions/focus.coffee#L11 which does contain a Cypress.log
.
Playing around a bit, I commented out the cy.now('focus', cy.$$(newEl))
and instead did:
return cy.$$(newElm).focus();
With that change, all logging goes away. To bring it back (always), we can add explicit logging:
const log = Cypress.log({
$el: cy.$$(subject || win.document.activeElement),
consoleProps: () => {
'Applied To': Array.from(subject), // convert jQuery nodeList to normal array for logging
// additional props can go here
}
})
// get a "before" snapshot
log.snapshot('before', { next: 'after' });
// this replaces the return of this command:
return new Promise((resolve) => {
doc.defaultView.requestAnimationFrame(resolve)
}).then(() => {
// return Promise.try(() => {
return keydown(activeElement, options, simulatedDefault, () => cy.$$(doc.activeElement))
}).then((el) => {
// Set the new element and finalize the snapshot
log.set('$el', el).snapshot();
return el;
}).finally(() => {
keyup(activeElement, options)
log.end()
})
I also added the cy.$$(doc.activeElement)
because a non-jQuery-wrapped subject was being returned which cause problems in certain cases.
Cypress does some magic and I can't remember if a log automatically happens on error or not. I don't have a log.snapshot()
on failure explicitly here, but there is one on success. There will be a Cypress DOM snapshot before and after the tab key was pressed, which should show any DOM diffs of any focus
or blur
handlers applied to the elements. Also the before
snapshot will have the element that previously had focus and the after snapshot will have the element that focus went to.
I could make a PR with this change, for now I'm wrapping for this logging.
Right now I'm using the following to add this functionality:
Cypress.Commands.overwrite('tab', (originalFn, subject) => {
const prevSubject = cy.$$(subject || cy.state('window').document.activeElement);
const log = Cypress.log({
$el: prevSubject,
consoleProps() {
return {
'Applied To': prevSubject.toArray()[0],
};
},
});
log.snapshot('before', {next: 'after'});
return Cypress.Promise.try(() => originalFn(subject))
.then(value => {
log.set('$el', value).snapshot();
return value;
})
.finally(() => {
log.end();
});
});
I found this issue hitting some edge cases in our CI (I assume timing related). On debugging I found an issue where the nextItemFromIndex
function doesn't return the correct element if it comes to the end of a sequence (off by one error).
https://github.com/Bkucera/cypress-plugin-tab/blob/master/src/index.js#L76-L94
const seq = [1, 2, 3, 4];
const nextItem = nextItemFromIndex(3, seq, false);
console.log(nextItem); // undefined
const previousItem = nextItemFromIndex(0, seq, true);
console.log(nextItem); // undefined
What happens is 3 is not the length of the array, so the if (i === seq.length)
is not true. Even if it was, the logic is still wrong, it would have to set i = -1
instead, because i
is getting incremented.
The correct implementation should be:
const nextItemFromIndex = (i, seq, reverse) => {
let nextIndex;
if (reverse) {
nextIndex = i - 1;
if (nextIndex < 0) {
nextIndex = seq.length - 1;
}
} else {
nextIndex = i + 1;
if (nextIndex === seq.length) {
nextIndex = 0;
}
}
return seq[nextIndex];
}
Or a bit more succinct:
const nextItemFromIndex = (i, seq, reverse) => {
if (reverse) {
const nextIndex = i === 0 ? seq.length - 1 : i - 1;
return seq[nextIndex];
} else {
const nextIndex = i === seq.length - 1 ? 0 : i + 1;
return seq[nextIndex];
}
}
I'm writing Cypress tests for my React Time Input Polyfill at the moment.
https://dan503.github.io/react-time-input-polyfill/
Part of the functionality is that when you press the [Tab]
key, it goes to the next segment rather than the next focus-able element.
In order to do that, I need to prevent the default tab functionality from firing using event.preventDefault()
.
This plugin doesn't seem to support the event.preventDefault()
use case though. ๐
This issue has plagued me for over a year. Sometimes using cy.tab()
or cy.tab({ shift: true })
would fail to transfer focus correctly. Running a bunch of console logs in this plugin and the ally.js
package it depends on revealed tabsequence
needs to find out what elements support focus.
if (!supports) {
supports = _supports();
}
It does this by calling .focus()
on a bunch of elements to see what elements can receive it.
focus && focus.focus && focus.focus();
// validate test's result
return options.validate
? options.validate(element, focus, data.document)
: data.document.activeElement === focus;
In order to not call focus on a bunch of elements every time tabsequence
is called, it caches focus tests.
if (supportsCache) {
return supportsCache;
}
I'm not sure why the cache is primed sometimes and not others, but my tests only pass when the cache is primed. If the cache is not primed, an element is focused, cy.tab()
is called, the focus support functions run and change the focused element (body in my case) and then cy.tab
performs the focus. In this case, the sequence is off because body
is detected as the activeElement
.
If the cache is primed, ally.js
does not run focus tests and cy.tab
performs like it should.
The solution I found that works is to override cy.visit
to prime the cache before any tests have a chance to run:
// cypress/commands.js or cypress/commands.ts
const supports = require('ally.js/supports/supports');
Cypress.Commands.overwrite('visit', (originalFn, url, options = {}) => {
if (typeof url === 'object') {
url = options.url;
}
return originalFn(url, {
...options,
onBeforeLoad(win) {
options.onBeforeLoad?.(win);
supports(); // prime the ally.js supports cache so it doesn't mess with the cypress-plugin-tab
},
});
});
This solution only works for tests that load pages using cy.visit
. If a Cypress tests clicks a link to navigate, the cache won't be primed. The other method is to add the supports()
code to the JS of the page and use if (window.Cypress) { supports() }
somewhere.
I'm not sure there's anything this plugin can do to fix this in all cases, but I figured I'd leave a fix in case others run into the same issue.
We have a focus redirect JS function for managing focus of a non-modal dialog. According to WCAG focus requirements, a non-modal dialog should be in the focus order after a triggering button:
https://www.w3.org/TR/UNDERSTANDING-WCAG20/navigation-mechanisms-focus-order.html
A Web page implements modeless dialogs via scripting. When the trigger button is activated, a dialog opens. The interactive elements in the dialog are inserted in the focus order immediately after the button. When the dialog is open, the focus order goes from the button to the elements of the dialog, then to the interactive element following the button. When the dialog is closed, the focus order goes from the button to the following element.
Since we "portal" our dialog, we manage focus manually. When we're on the last focusable element in a dialog, a keydown
handler will close the dialog and shift focus back to the triggering button. When the keydown
event is completed, the browser will perform the default action of advancing focus. This plugin uses the previous el
to determine the next focusable element before the keydown
event is processed. This means this plugin doesn't account for focus changes inside a keydown
which is a valid thing to do.
If instead doc.activeElement
was used to determine index
, this use-case would work.
But this breaks the documented API where focus()
is not necessary to tab.
Example, in the tests:
cy.get('a:first').tab()
This example allows you to skip focusing on the a:first
element and tab from it anyway, which is not how users really interact with a page. With this change, the test would have to be changed to:
cy.get('a:first').focus().tab()
@bkucera What do you think? My proposal is more realistic to how browsers work, but would be a breaking change according to the documented API.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.