diff --git a/packages/driver/src/cy/keyboard.ts b/packages/driver/src/cy/keyboard.ts index 198cfaea1f89..588204d0c785 100644 --- a/packages/driver/src/cy/keyboard.ts +++ b/packages/driver/src/cy/keyboard.ts @@ -211,7 +211,7 @@ const getKeyDetails = (onKeyNotFound) => { onKeyNotFound(key, _.keys(getKeymap()).join(', ')) - throw Error(`Not a valid key: ${key}`) + throw new Error('this can never happen') } } @@ -280,7 +280,8 @@ const validateTyping = ( keys: KeyDetails[], currentIndex: number, onFail: Function, - skipCheckUntilIndex?: number, + skipCheckUntilIndex: number | undefined, + force: boolean ) => { const chars = joinKeyArrayToString(keys.slice(currentIndex)) const allChars = joinKeyArrayToString(keys) @@ -328,15 +329,20 @@ const validateTyping = ( return {} } - if (!isFocusable) { + // throw error if element, which is normally typeable, is disabled for some reason + // don't throw if force: true + if (!isFocusable && isTextLike && !force) { const node = $dom.stringify($el) - if (isTextLike) { - $utils.throwErrByPath('type.not_actionable_textlike', { - onFail, - args: { node }, - }) - } + $utils.throwErrByPath('type.not_actionable_textlike', { + onFail, + args: { node }, + }) + } + + // throw error if element cannot receive keyboard events under any conditions + if (!isFocusable && !isTextLike) { + const node = $dom.stringify($el) $utils.throwErrByPath('type.not_on_typeable_element', { onFail, @@ -660,6 +666,7 @@ export class Keyboard { currentKeyIndex, options.onFail, _skipCheckUntilIndex, + options.force ) _skipCheckUntilIndex = skipCheckUntilIndex diff --git a/packages/driver/src/dom/selection.ts b/packages/driver/src/dom/selection.ts index 5f210a3e9679..bf70d7522d6c 100644 --- a/packages/driver/src/dom/selection.ts +++ b/packages/driver/src/dom/selection.ts @@ -154,6 +154,13 @@ const setSelectionRange = function (el, start, end) { $elements.callNativeMethod(el, 'setSelectionRange', start, end) } +// Whether or not the selection contains any text +// since Selection.isCollapsed will be true when selection +// is inside non-selectionRange input (e.g. input[type=email]) +const isSelectionCollapsed = function (selection: Selection) { + return !selection.toString() +} + const deleteRightOfCursor = function (el) { if ($elements.canSetSelectionRangeElement(el)) { const { start, end } = getSelectionBounds(el) @@ -167,7 +174,7 @@ const deleteRightOfCursor = function (el) { const selection = _getSelectionByEl(el) - if (selection.isCollapsed) { + if (isSelectionCollapsed(selection)) { $elements.callNativeMethod( selection, 'modify', @@ -195,7 +202,7 @@ const deleteLeftOfCursor = function (el) { const selection = _getSelectionByEl(el) - if (selection.isCollapsed) { + if (isSelectionCollapsed(selection)) { $elements.callNativeMethod( selection, 'modify', diff --git a/packages/driver/test/cypress/integration/commands/actions/type_spec.js b/packages/driver/test/cypress/integration/commands/actions/type_spec.js index 0fa74a8cf242..0b518ecd7a05 100644 --- a/packages/driver/test/cypress/integration/commands/actions/type_spec.js +++ b/packages/driver/test/cypress/integration/commands/actions/type_spec.js @@ -183,7 +183,7 @@ describe('src/cy/commands/actions/type', () => { }) describe('actionability', () => { - it('can forcibly click even when element is invisible', () => { + it('can forcibly type + click even when element is invisible', () => { const $txt = cy.$$(':text:first').hide() expect($txt).not.to.have.value('foo') @@ -199,7 +199,13 @@ describe('src/cy/commands/actions/type', () => { }) }) - it('can forcibly click even when being covered by another element', () => { + it('can force type when element is disabled', function () { + cy.$$('input:text:first').prop('disabled', true) + cy.get('input:text:first').type('foo', { force: true }) + .should('have.value', 'foo') + }) + + it('can forcibly type + click even when being covered by another element', () => { const $input = $('') .attr('id', 'input-covered-in-span') .css({ @@ -1810,6 +1816,13 @@ describe('src/cy/commands/actions/type', () => { }) }) + it('can delete all with {selectall}{backspace} in non-selectionrange element', () => { + cy.get('input[type=email]:first') + .should(($el) => $el.val('sdfsdf')) + .type('{selectall}{backspace}') + .should('have.value', '') + }) + it('can backspace a selection range of characters', () => { // select the 'ar' characters cy @@ -4415,22 +4428,6 @@ describe('src/cy/commands/actions/type', () => { cy.get('input:text:first').type('foo') }) - it('throws when subject is disabled and force:true', function (done) { - cy.timeout(200) - - cy.$$('input:text:first').prop('disabled', true) - - cy.on('fail', (err) => { - // get + type logs - expect(this.logs.length).eq(2) - expect(err.message).to.include('cy.type() failed because it targeted a disabled element.') - - done() - }) - - cy.get('input:text:first').type('foo', { force: true }) - }) - it('throws when submitting within nested forms') it('logs once when not dom subject', function (done) { @@ -4532,11 +4529,11 @@ https://on.cypress.io/type`) }) }) - describe('naughtly strings', () => { + describe('naughty strings', () => { _.each(['Ω≈ç√∫˜µ≤≥÷', '2.2250738585072011e-308', '田中さんにあげて下さい', '', '⁰⁴⁵₀₁₂', '🐵 🙈 🙉 🙊', '', '$USER'], (val) => { - it(`allows typing some naughtly strings (${val})`, () => { + it(`allows typing some naughty strings (${val})`, () => { cy .get(':text:first').type(val) .should('have.value', val)