Skip to content

Instantly share code, notes, and snippets.

@samtsai
Forked from erquhart/selection-command.js
Last active March 6, 2021 19:37
Show Gist options
  • Select an option

  • Save samtsai/5cf901ba61fd8d44c8dd7eaa728cac49 to your computer and use it in GitHub Desktop.

Select an option

Save samtsai/5cf901ba61fd8d44c8dd7eaa728cac49 to your computer and use it in GitHub Desktop.

Revisions

  1. samtsai revised this gist Mar 10, 2020. 1 changed file with 94 additions and 52 deletions.
    146 changes: 94 additions & 52 deletions selection-command.js
    Original file line number Diff line number Diff line change
    @@ -37,78 +37,120 @@
    */

    // Low level command reused by `setSelection` and low level command `setCursor`
    Cypress.Commands.add('selection', { prevSubject: true }, (subject, fn) => {
    Cypress.Commands.add("selection", { prevSubject: true }, (subject, fn) => {
    cy.wrap(subject)
    .trigger('mousedown')
    .trigger("mousedown")
    .then(fn)
    .trigger('mouseup');
    .trigger("mouseup")

    cy.document().trigger('selectionchange');
    return cy.wrap(subject);
    });

    Cypress.Commands.add('setSelection', { prevSubject: true }, (subject, query, endQuery) => {
    cy.document().trigger("selectionchange")
    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);
    })

    Cypress.Commands.add(
    "setSelection",
    { prevSubject: true },
    (subject, query, endQuery) => {
    return cy.wrap(subject).selection($el => {
    const el = $el[0]
    if (isInputOrTextArea(el)) {
    const text = $el.text()
    const start = text.indexOf(query)
    const end = endQuery
    ? text.indexOf(endQuery) + endQuery.length
    : start + query.length

    $el[0].setSelectionRange(start, end)
    } else if (typeof query === "string") {
    const anchorNode = getTextNode(el, query)
    const focusNode = endQuery ? getTextNode(el, 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 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);
    Cypress.Commands.add(
    "setCursor",
    { prevSubject: true },
    (subject, query, atStart) => {
    return cy.wrap(subject).selection($el => {
    const el = $el[0]

    if (isInputOrTextArea(el)) {
    const text = $el.text()
    const position = text.indexOf(query) + (atStart ? 0 : query.length)
    $el[0].setSelectionRange(position, position)
    } else {
    const node = getTextNode(el, 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(
    "setCursorBefore",
    { prevSubject: true },
    (subject, query) => {
    cy.wrap(subject).setCursor(query, true)
    }
    )

    Cypress.Commands.add('setCursorAfter', { prevSubject: true }, (subject, query) => {
    cy.wrap(subject).setCursor(query);
    });
    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);
    function getTextNode(el, match) {
    const walk = document.createTreeWalker(el, NodeFilter.SHOW_TEXT, null, false)
    if (!match) {
    return walk.nextNode();
    return walk.nextNode()
    }

    const nodes = [];
    let node;
    while(node = walk.nextNode()) {
    let node
    while ((node = walk.nextNode())) {
    if (node.wholeText.includes(match)) {
    return node;
    return node
    }
    }
    }

    function setBaseAndExtent(...args) {
    const document = args[0].ownerDocument;
    document.getSelection().removeAllRanges();
    document.getSelection().setBaseAndExtent(...args);
    }
    const node = args[0]
    const document = node.ownerDocument
    const selection = document.getSelection()
    selection.setBaseAndExtent(...args)
    }

    function isInputOrTextArea(el) {
    return (
    el instanceof HTMLInputElement ||
    el instanceof HTMLTextAreaElement ||
    el.nodeName === "TEXTAREA" ||
    el.nodeName === "INPUT"
    )
    }
  2. @erquhart erquhart revised this gist Apr 12, 2019. 1 changed file with 3 additions and 1 deletion.
    4 changes: 3 additions & 1 deletion selection-command.js
    Original file line number Diff line number Diff line change
    @@ -77,8 +77,10 @@ Cypress.Commands.add('setCursor', { prevSubject: true }, (subject, query, atStar
    const offset = node.wholeText.indexOf(query) + (atStart ? 0 : query.length);
    const document = node.ownerDocument;
    document.getSelection().removeAllRanges();
    document.getSelection().setBaseAndExtent(node, offset, node, offset);
    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) => {
  3. @erquhart erquhart created this gist Apr 11, 2019.
    112 changes: 112 additions & 0 deletions selection-command.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,112 @@
    /**
    * 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().setBaseAndExtent(node, offset, node, offset);
    })
    });

    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);
    }