Last active
December 11, 2024 03:28
-
-
Save erquhart/37bf2d938ab594058e0572ed17d3837a to your computer and use it in GitHub Desktop.
Text selection commands for Cypress.io
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Credits | |
* @Bkucera: https://github.com/cypress-io/cypress/issues/2839#issuecomment-447012818 | |
* @Phrogz: https://stackoverflow.com/a/10730777/1556245 | |
* | |
* Usage | |
* ``` | |
* // Types "foo" and then selects "fo" | |
* cy.get('input') | |
* .type('foo') | |
* .setSelection('fo') | |
* | |
* // Types "foo", "bar", "baz", and "qux" on separate lines, then selects "foo", "bar", and "baz" | |
* cy.get('textarea') | |
* .type('foo{enter}bar{enter}baz{enter}qux{enter}') | |
* .setSelection('foo', 'baz') | |
* | |
* // Types "foo" and then sets the cursor before the last letter | |
* cy.get('input') | |
* .type('foo') | |
* .setCursorAfter('fo') | |
* | |
* // Types "foo" and then sets the cursor at the beginning of the word | |
* cy.get('input') | |
* .type('foo') | |
* .setCursorBefore('foo') | |
* | |
* // `setSelection` can alternatively target starting and ending nodes using query strings, | |
* // plus specific offsets. The queries are processed via `Element.querySelector`. | |
* cy.get('body') | |
* .setSelection({ | |
* anchorQuery: 'ul > li > p', // required | |
* anchorOffset: 2 // default: 0 | |
* focusQuery: 'ul > li > p:last-child', // default: anchorQuery | |
* focusOffset: 0 // default: 0 | |
* }) | |
*/ | |
// Low level command reused by `setSelection` and low level command `setCursor` | |
Cypress.Commands.add('selection', { prevSubject: true }, (subject, fn) => { | |
cy.wrap(subject) | |
.trigger('mousedown') | |
.then(fn) | |
.trigger('mouseup'); | |
cy.document().trigger('selectionchange'); | |
return cy.wrap(subject); | |
}); | |
Cypress.Commands.add('setSelection', { prevSubject: true }, (subject, query, endQuery) => { | |
return cy.wrap(subject) | |
.selection($el => { | |
if (typeof query === 'string') { | |
const anchorNode = getTextNode($el[0], query); | |
const focusNode = endQuery ? getTextNode($el[0], endQuery) : anchorNode; | |
const anchorOffset = anchorNode.wholeText.indexOf(query); | |
const focusOffset = endQuery ? | |
focusNode.wholeText.indexOf(endQuery) + endQuery.length : | |
anchorOffset + query.length; | |
setBaseAndExtent(anchorNode, anchorOffset, focusNode, focusOffset); | |
} else if (typeof query === 'object') { | |
const el = $el[0]; | |
const anchorNode = getTextNode(el.querySelector(query.anchorQuery)); | |
const anchorOffset = query.anchorOffset || 0; | |
const focusNode = query.focusQuery ? getTextNode(el.querySelector(query.focusQuery)) : anchorNode; | |
const focusOffset = query.focusOffset || 0; | |
setBaseAndExtent(anchorNode, anchorOffset, focusNode, focusOffset); | |
} | |
}); | |
}); | |
// Low level command reused by `setCursorBefore` and `setCursorAfter`, equal to `setCursorAfter` | |
Cypress.Commands.add('setCursor', { prevSubject: true }, (subject, query, atStart) => { | |
return cy.wrap(subject) | |
.selection($el => { | |
const node = getTextNode($el[0], query); | |
const offset = node.wholeText.indexOf(query) + (atStart ? 0 : query.length); | |
const document = node.ownerDocument; | |
document.getSelection().removeAllRanges(); | |
document.getSelection().collapse(node, offset); | |
}) | |
// Depending on what you're testing, you may need to chain a `.click()` here to ensure | |
// further commands are picked up by whatever you're testing (this was required for Slate, for example). | |
}); | |
Cypress.Commands.add('setCursorBefore', { prevSubject: true }, (subject, query) => { | |
cy.wrap(subject).setCursor(query, true); | |
}); | |
Cypress.Commands.add('setCursorAfter', { prevSubject: true }, (subject, query) => { | |
cy.wrap(subject).setCursor(query); | |
}); | |
// Helper functions | |
function getTextNode(el, match){ | |
const walk = document.createTreeWalker(el, NodeFilter.SHOW_TEXT, null, false); | |
if (!match) { | |
return walk.nextNode(); | |
} | |
const nodes = []; | |
let node; | |
while(node = walk.nextNode()) { | |
if (node.wholeText.includes(match)) { | |
return node; | |
} | |
} | |
} | |
function setBaseAndExtent(...args) { | |
const document = args[0].ownerDocument; | |
document.getSelection().removeAllRanges(); | |
document.getSelection().setBaseAndExtent(...args); | |
} |
When I chain setSelection, setCursorBefore or setCursorAfter with .type() there is a 50/50 chance that .type() will occur first. Is there a way to fix this?
Thanks ! It works well for what I needed
guys don't waste your time by trying to use the code. it throws the following error:
TypeError: Cannot read property 'wholeText' of undefined
@alexey-sh Were you able to solve this issue? I am facing the same. If you solved it, can you please tell me what changes did you make?
@rahulworks-git I don't remember exact way to solve it but I think it was fixed somehow. May by I decided to skip this kind of tests.
Thank you, friend. I've been trying to highlight the damn text in the cypress tree for two days.
Astonished that this is still useful!
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
does this (or the newer solution here in the comments) work on plain text in the DOM? (and not only on input)
for example if I have:
<p> this is the text I have <p>
can I select "is the"?