diff --git a/package.json b/package.json index 42d7ec32296..b123bf41856 100644 --- a/package.json +++ b/package.json @@ -114,7 +114,7 @@ "source-map-support": "^0.5.5", "strip-bom": "^2.0.0", "testcafe-browser-tools": "1.6.8", - "testcafe-hammerhead": "14.6.9", + "testcafe-hammerhead": "14.6.10", "testcafe-legacy-api": "3.1.11", "testcafe-reporter-json": "^2.1.0", "testcafe-reporter-list": "^2.1.0", diff --git a/src/client/automation/playback/click/click-command.js b/src/client/automation/playback/click/click-command.js index 52a248df8d9..f66ed4a78f6 100644 --- a/src/client/automation/playback/click/click-command.js +++ b/src/client/automation/playback/click/click-command.js @@ -27,6 +27,36 @@ class ElementClickCommand { } } +class LabelElementClickCommand extends ElementClickCommand { + constructor (eventState, eventArgs) { + super(eventState, eventArgs); + + this.label = this.eventArgs.element; + this.input = getElementBoundToLabel(this.eventArgs.element); + } + + run () { + let focusRaised = false; + + const ensureFocusRaised = e => { + focusRaised = e.target === this.input; + }; + + listeners.addInternalEventListener(window, ['focus'], ensureFocusRaised); + + super.run(); + + listeners.removeInternalEventListener(window, ['focus'], ensureFocusRaised); + + if (domUtils.isElementFocusable(this.label) && !focusRaised) + this._ensureBoundElementFocusRaised(); + } + + _ensureBoundElementFocusRaised () { + eventSimulator.focus(this.input); + } +} + class SelectElementClickCommand extends ElementClickCommand { constructor (eventState, eventArgs) { super(eventState, eventArgs); @@ -63,11 +93,11 @@ class OptionElementClickCommand extends ElementClickCommand { } } -class LabelledCheckboxElementClickCommand extends ElementClickCommand { +class LabelledCheckboxElementClickCommand extends LabelElementClickCommand { constructor (eventState, eventArgs) { super(eventState, eventArgs); - this.checkbox = getElementBoundToLabel(this.eventArgs.element); + this.checkbox = this.input; } run () { @@ -98,6 +128,7 @@ export default function (eventState, eventArgs) { const elementBoundToLabel = getElementBoundToLabel(eventArgs.element); const isSelectElement = domUtils.isSelectElement(eventArgs.element); const isOptionElement = domUtils.isOptionElement(eventArgs.element); + const isLabelElement = domUtils.isLabelElement(eventArgs.element) && elementBoundToLabel; const isLabelledCheckbox = elementBoundToLabel && domUtils.isCheckboxElement(elementBoundToLabel); if (isSelectElement) @@ -109,6 +140,9 @@ export default function (eventState, eventArgs) { if (isLabelledCheckbox) return new LabelledCheckboxElementClickCommand(eventState, eventArgs); + if (isLabelElement) + return new LabelElementClickCommand(eventState, eventArgs); + return new ElementClickCommand(eventState, eventArgs); } diff --git a/src/client/automation/utils/utils.js b/src/client/automation/utils/utils.js index b4b16079608..85eafad9d69 100644 --- a/src/client/automation/utils/utils.js +++ b/src/client/automation/utils/utils.js @@ -43,9 +43,9 @@ export function focusAndSetSelection (element, simulateFocus, caretPos) { const isTextEditable = domUtils.isTextEditableElement(element); const labelWithForAttr = domUtils.closest(element, 'label[for]'); const isElementFocusable = domUtils.isElementFocusable(element); - const shouldFocusByRelatedElement = !domUtils.isElementFocusable(element) && labelWithForAttr; + const shouldFocusByRelatedElement = labelWithForAttr; const isContentEditable = domUtils.isContentEditableElement(element); - let elementForFocus = isContentEditable ? contentEditable.findContentEditableParent(element) : element; + let elementForFocus = isContentEditable ? contentEditable.findContentEditableParent(element) : element; // NOTE: in WebKit, if selection was never set in an input element, the focus method selects all the // text in this element. So, we should call select before focus to set the caret to the first symbol. @@ -55,7 +55,7 @@ export function focusAndSetSelection (element, simulateFocus, caretPos) { // NOTE: we should call focus for the element related with a 'label' that has the 'for' attribute if (shouldFocusByRelatedElement) { if (simulateFocus) - focusByRelatedElement(labelWithForAttr); + focusByLabel(labelWithForAttr); resolve(); return; @@ -63,7 +63,7 @@ export function focusAndSetSelection (element, simulateFocus, caretPos) { const focusWithSilentMode = !simulateFocus; const focusForMouseEvent = true; - let preventScrolling = false; + let preventScrolling = false; if (!isElementFocusable && !isContentEditable) { const curDocument = domUtils.findDocument(elementForFocus); @@ -107,12 +107,19 @@ export function focusAndSetSelection (element, simulateFocus, caretPos) { export function getElementBoundToLabel (element) { const labelWithForAttr = domUtils.closest(element, 'label[for]'); - const control = labelWithForAttr && labelWithForAttr.control; + const control = labelWithForAttr && (labelWithForAttr.control || document.getElementById(labelWithForAttr.htmlFor)); const isControlVisible = control && styleUtils.isElementVisible(control); return isControlVisible ? control : null; } +export function focusByLabel (label) { + if (domUtils.isElementFocusable(label)) + focusBlurSandbox.focus(label, testCafeCore.noop, false, true); + else + focusByRelatedElement(label); +} + export function focusByRelatedElement (element) { const elementForFocus = getElementBoundToLabel(element); diff --git a/src/client/core/utils/dom.js b/src/client/core/utils/dom.js index 32aac511692..92b51804fa3 100644 --- a/src/client/core/utils/dom.js +++ b/src/client/core/utils/dom.js @@ -25,6 +25,7 @@ export const isTextAreaElement = hammerhead.utils.dom.isTex export const isAnchorElement = hammerhead.utils.dom.isAnchorElement; export const isImgElement = hammerhead.utils.dom.isImgElement; export const isFormElement = hammerhead.utils.dom.isFormElement; +export const isLabelElement = hammerhead.utils.dom.isLabelElement; export const isSelectElement = hammerhead.utils.dom.isSelectElement; export const isRadioButtonElement = hammerhead.utils.dom.isRadioButtonElement; export const isColorInputElement = hammerhead.utils.dom.isColorInputElement; diff --git a/test/client/before-test.js b/test/client/before-test.js index 39c8ffa6d6f..aaca0a839a2 100644 --- a/test/client/before-test.js +++ b/test/client/before-test.js @@ -102,12 +102,12 @@ // With this hack, we only allow setting the scroll by a script and prevent native browser scrolling. if (hammerhead.utils.browser.isIOS) { document.addEventListener('DOMContentLoaded', function () { - const originWindowScrollTo = window.scrollTo; + const originWindowScrollTo = hammerhead.nativeMethods.scrollTo; let lastScrollTop = window.scrollY; let lastScrollLeft = window.scrollX; - window.scrollTo = function () { + hammerhead.nativeMethods.scrollTo = function () { lastScrollLeft = arguments[0]; lastScrollTop = arguments[1]; diff --git a/test/client/fixtures/automation/click-test.js b/test/client/fixtures/automation/click-test.js index c4d404a83d9..3e67840b43b 100644 --- a/test/client/fixtures/automation/click-test.js +++ b/test/client/fixtures/automation/click-test.js @@ -318,7 +318,7 @@ $(document).ready(function () { backgroundColor: '#ff0000' }); - window.scrollTo(0, 5050); + hammerhead.nativeMethods.scrollTo.call(window, 0, 5050); const click = new ClickAutomation(target[0], { offsetX: 10, @@ -364,7 +364,7 @@ $(document).ready(function () { height: 100 }); - window.scrollTo(0, 5050); + hammerhead.nativeMethods.scrollTo.call(window, 0, 5050); const click = new ClickAutomation(target[0], { offsetX: 10, diff --git a/test/functional/fixtures/regression/gh-1057/pages/hiddenByFixedParent.html b/test/functional/fixtures/regression/gh-1057/pages/hiddenByFixedParent.html index cad9ce838f3..f2794e6533b 100644 --- a/test/functional/fixtures/regression/gh-1057/pages/hiddenByFixedParent.html +++ b/test/functional/fixtures/regression/gh-1057/pages/hiddenByFixedParent.html @@ -48,12 +48,13 @@ // NOTE: Try to avoid odd scrolls in iOS on sauceLabs if (isIOS) { - const originWindowScrollTo = window.scrollTo; + const nativeMethods = window['%hammerhead%'].nativeMethods; + const originWindowScrollTo = nativeMethods.scrollTo; let lastScrollTop = window.scrollY; let lastScrollLeft = window.scrollX; - window.scrollTo = function () { + nativeMethods.scrollTo = function () { lastScrollLeft = arguments[0]; lastScrollTop = arguments[1]; diff --git a/test/functional/fixtures/regression/gh-1353/pages/index.html b/test/functional/fixtures/regression/gh-1353/pages/index.html index 71487b150fe..7641aaabafa 100644 --- a/test/functional/fixtures/regression/gh-1353/pages/index.html +++ b/test/functional/fixtures/regression/gh-1353/pages/index.html @@ -30,16 +30,17 @@ // NOTE: scrolling has issues in iOS Simulator https://github.com/DevExpress/testcafe/issues/1237 + + diff --git a/test/functional/fixtures/regression/gh-3501/test.js b/test/functional/fixtures/regression/gh-3501/test.js new file mode 100644 index 00000000000..9aa68d272ae --- /dev/null +++ b/test/functional/fixtures/regression/gh-3501/test.js @@ -0,0 +1,11 @@ +describe('[Regression](GH-3501) - Should focus label if it is bound to element and has tabIndex attribute', function () { + it('Click label bound to radio', function () { + return runTests('testcafe-fixtures/index.js', 'Label bound to radio is focused'); + }); + + it('Click label bound to checkbox', function () { + return runTests('testcafe-fixtures/index.js', 'Label bound to checkbox is focused'); + }); +}); + + diff --git a/test/functional/fixtures/regression/gh-3501/testcafe-fixtures/index.js b/test/functional/fixtures/regression/gh-3501/testcafe-fixtures/index.js new file mode 100644 index 00000000000..2814a141659 --- /dev/null +++ b/test/functional/fixtures/regression/gh-3501/testcafe-fixtures/index.js @@ -0,0 +1,38 @@ +fixture `Should focus label if it is bound to element and has tabIndex attribute` + .page `http://localhost:3000/fixtures/regression/gh-3501/pages/index.html`; + +import { Selector, ClientFunction } from 'testcafe'; + +const label1 = Selector('#label1'); +const label2 = Selector('#label2'); + +const getLog = ClientFunction(() => { + return window.eventLog; +}); + +test(`Label bound to radio is focused`, async t => { + await t.click(label1); + + const log = await getLog(); + + await t.expect(log).eql([ + 'focus. Target: label1', + 'click. Target: label1', + 'focus. Target: radio' + ]); + + +}); + +test(`Label bound to checkbox is focused`, async t => { + await t.click(label2); + + const log = await getLog(); + + await t.expect(log).eql([ + 'focus. Target: label2', + 'click. Target: label2', + 'focus. Target: checkbox' + ]); +}); + diff --git a/test/functional/fixtures/regression/gh-883/pages/index.html b/test/functional/fixtures/regression/gh-883/pages/index.html index e00c0366654..c76bbc30d9a 100644 --- a/test/functional/fixtures/regression/gh-883/pages/index.html +++ b/test/functional/fixtures/regression/gh-883/pages/index.html @@ -13,12 +13,13 @@ // NOTE: Try to avoid odd scrolls in iOS on sauceLabs if (isIOS) { - const originWindowScrollTo = window.scrollTo; + const nativeMethods = window['%hammerhead%'].nativeMethods; + const originWindowScrollTo = nativeMethods.scrollTo; let lastScrollTop = window.scrollY; let lastScrollLeft = window.scrollX; - window.scrollTo = function () { + nativeMethods.scrollTo = function () { lastScrollLeft = arguments[0]; lastScrollTop = arguments[1]; diff --git a/test/functional/fixtures/regression/gh-973/pages/index.html b/test/functional/fixtures/regression/gh-973/pages/index.html index c72de539a6c..7cee3300db7 100644 --- a/test/functional/fixtures/regression/gh-973/pages/index.html +++ b/test/functional/fixtures/regression/gh-973/pages/index.html @@ -29,12 +29,13 @@ // NOTE: Try to avoid odd scrolls in iOS on sauceLabs if (isIOS) { - const originWindowScrollTo = window.scrollTo; + const nativeMethods = window['%hammerhead%'].nativeMethods; + const originWindowScrollTo = nativeMethods.scrollTo; let lastScrollTop = window.scrollY; let lastScrollLeft = window.scrollX; - window.scrollTo = function () { + nativeMethods.scrollTo = function () { lastScrollLeft = arguments[0]; lastScrollTop = arguments[1];