Skip to content

Commit

Permalink
feat: upgraded visibility errors (#7319)
Browse files Browse the repository at this point in the history
<!--
Thank you for your contribution.

Before making a PR, please read our contributing guidelines at

https://github.com/DevExpress/testcafe/blob/master/CONTRIBUTING.md#code-contribution

We recommend creating a *draft* PR, so that you can mark it as 'ready
for review' when you are done.
-->
[closes #7310]

## Purpose
Show detailed information in errors when elements are not visible.  

## Approach
1. Research visibility cases
2. Refactor current visibility checks
3. Add tests
4. Add check when an element has the property `visibility: collapse`
5. Add rendering messages with detailed visibility information
6. Expand current errors templates with rendered messages

## References
#7310 

## Pre-Merge TODO
- [X] Write tests for your proposed changes
- [x] Make sure that existing tests do not fail

Co-authored-by: titerman <43554315+titerman@users.noreply.github.com>
  • Loading branch information
Aleksey28 and titerman committed Nov 2, 2022
1 parent 0ce9a64 commit 38e2150
Show file tree
Hide file tree
Showing 40 changed files with 628 additions and 223 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@
"strip-bom": "^2.0.0",
"testcafe-browser-tools": "2.0.23",
"testcafe-hammerhead": "28.0.0",
"testcafe-legacy-api": "5.1.5",
"testcafe-legacy-api": "5.1.6",
"testcafe-reporter-dashboard": "^0.2.7-rc.1",
"testcafe-reporter-json": "^2.1.0",
"testcafe-reporter-list": "^2.1.0",
Expand Down
2 changes: 0 additions & 2 deletions src/client/automation/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import SHORTCUT_TYPE from './playback/press/shortcut-type';
import { getSelectionCoordinatesByPosition } from './playback/select/utils';
import getElementFromPoint from './get-element';
import calculateSelectTextArguments from './playback/select/calculate-select-text-arguments';
import ERROR_TYPES from '../../shared/errors/automation-errors';
import cursor from './cursor';
import MoveAutomation from './move';

Expand All @@ -52,7 +51,6 @@ exports.MouseOptions = MouseOptions;
exports.ClickOptions = ClickOptions;
exports.TypeOptions = TypeOptions;

exports.ERROR_TYPES = ERROR_TYPES;
exports.AutomationSettings = AutomationSettings;
exports.getOffsetOptions = getOffsetOptions;
exports.calculateSelectTextArguments = calculateSelectTextArguments;
Expand Down
7 changes: 4 additions & 3 deletions src/client/automation/playback/click/click-command.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ const eventSimulator = hammerhead.eventSandbox.eventSimulator;
const listeners = hammerhead.eventSandbox.listeners;
const nativeMethods = hammerhead.nativeMethods;

const domUtils = testCafeCore.domUtils;
const styleUtils = testCafeCore.styleUtils;
const domUtils = testCafeCore.domUtils;
const styleUtils = testCafeCore.styleUtils;
const selectController = testCafeCore.selectController;

const selectElementUI = testCafeUI.selectElement;

Expand Down Expand Up @@ -76,7 +77,7 @@ class SelectElementClickCommand extends ElementClickCommand {
const isSelectWithDropDown = styleUtils.getSelectElementSize(element) === 1;

if (isSelectWithDropDown && this.eventState.simulateDefaultBehavior !== false) {
if (selectElementUI.isOptionListExpanded(element))
if (selectController.isOptionListExpanded(element))
selectElementUI.collapseOptionList();
else
selectElementUI.expandOptionList(element);
Expand Down
13 changes: 7 additions & 6 deletions src/client/automation/playback/click/select-child.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@ const eventSimulator = hammerhead.eventSandbox.eventSimulator;
const focusBlurSandbox = hammerhead.eventSandbox.focusBlur;
const nativeMethods = hammerhead.nativeMethods;

const domUtils = testCafeCore.domUtils;
const styleUtils = testCafeCore.styleUtils;
const delay = testCafeCore.delay;
const domUtils = testCafeCore.domUtils;
const styleUtils = testCafeCore.styleUtils;
const delay = testCafeCore.delay;
const selectController = testCafeCore.selectController;

const selectElementUI = testCafeUI.selectElement;

Expand All @@ -37,7 +38,7 @@ export default class SelectChildClickAutomation {
this.automationSettings = new AutomationSettings(clickOptions.speed);

this.parentSelect = domUtils.getSelectParent(this.element);
this.optionListExpanded = this.parentSelect ? selectElementUI.isOptionListExpanded(this.parentSelect) : false;
this.optionListExpanded = this.parentSelect ? selectController.isOptionListExpanded(this.parentSelect) : false;
this.childIndex = null;
this.clickCausesChange = false;

Expand All @@ -62,7 +63,7 @@ export default class SelectChildClickAutomation {
}

_calculateEventArguments () {
const childElement = this.optionListExpanded ? selectElementUI.getEmulatedChildElement(this.element) : this.element;
const childElement = this.optionListExpanded ? selectController.getEmulatedChildElement(this.element) : this.element;
const parentSelectSize = styleUtils.getSelectElementSize(this.parentSelect) > 1;

return {
Expand All @@ -77,7 +78,7 @@ export default class SelectChildClickAutomation {
let offsetY = null;

if (this.optionListExpanded) {
element = selectElementUI.getEmulatedChildElement(this.element);
element = selectController.getEmulatedChildElement(this.element);

const moveActionOffsets = getDefaultAutomationOffsets(element);

Expand Down
17 changes: 10 additions & 7 deletions src/client/automation/playback/select/base.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import * as selectUtils from './utils';
import MoveAutomation from '../move/move';
import { MoveOptions } from '../../../../test-run/commands/options';
import cursor from '../../cursor';
import AUTOMATION_ERROR_TYPES from '../../../../shared/errors/automation-errors';
import { ActionElementIsInvisibleError } from '../../../../shared/errors';

const Promise = hammerhead.Promise;
const browserUtils = hammerhead.utils.browser;
Expand Down Expand Up @@ -45,13 +45,16 @@ export default class SelectBaseAutomation extends VisibleElementAutomation {
};
}

static _calculateEventArguments (point) {
const clientPoint = positionUtils.offsetToClientCoords(point);
_calculateEventArguments () {
const clientPoint = positionUtils.offsetToClientCoords(this.clientPoint);

return getElementFromPoint(clientPoint)
.then(element => {
if (!element)
throw new Error(AUTOMATION_ERROR_TYPES.elementIsInvisibleError);
if (!element) {
throw new ActionElementIsInvisibleError(null, {
reason: positionUtils.getElOutsideBoundsReason(this.element),
});
}

return {
element: element,
Expand Down Expand Up @@ -109,7 +112,7 @@ export default class SelectBaseAutomation extends VisibleElementAutomation {
_mousedown () {
return cursor
.leftButtonDown()
.then(() => SelectBaseAutomation._calculateEventArguments(this.clientPoint))
.then(() => this._calculateEventArguments())
.then(args => {
this.eventArgs = args;

Expand Down Expand Up @@ -151,7 +154,7 @@ export default class SelectBaseAutomation extends VisibleElementAutomation {
.then(() => {
this._setSelection();

return SelectBaseAutomation._calculateEventArguments(this.clientPoint);
return this._calculateEventArguments();
})
.then(args => {
this.eventArgs = args;
Expand Down
17 changes: 10 additions & 7 deletions src/client/automation/visible-element-automation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import screenPointToClient from './utils/screen-point-to-client';
import getDevicePoint from './utils/get-device-point';
import { getOffsetOptions } from '../core/utils/offsets';
import getElementFromPoint from './get-element';
import AUTOMATION_ERROR_TYPES from '../../shared/errors/automation-errors';
import { ActionElementIsInvisibleError, ActionElementIsNotTargetError } from '../../shared/errors';
import AutomationSettings from './settings';
import MoveAutomation from './move';
import AxisValues, { AxisValuesData } from '../core/utils/values/axis-values';
Expand Down Expand Up @@ -215,26 +215,29 @@ export default class VisibleElementAutomation extends SharedEventEmitter {
});
}

private static _checkElementState (state: ElementState, useStrictElementCheck: boolean): ElementState {
if (!state.element)
throw new Error(AUTOMATION_ERROR_TYPES.elementIsInvisibleError);
private _checkElementState (state: ElementState, useStrictElementCheck: boolean): ElementState {
if (!state.element) {
throw new ActionElementIsInvisibleError(null, {
reason: positionUtils.getElOutsideBoundsReason(this.element),
});
}

if (useStrictElementCheck && (!state.isTarget || state.inMoving))
throw new Error(AUTOMATION_ERROR_TYPES.foundElementIsNotTarget);
throw new ActionElementIsNotTargetError();

return state;
}

protected _ensureElement (useStrictElementCheck: boolean, skipCheckAfterMoving = false, skipMoving = false): Promise<ElementStateArgsBase> {
return this
._wrapAction(() => this._scrollToElement())
.then(state => VisibleElementAutomation._checkElementState(state, useStrictElementCheck))
.then(state => this._checkElementState(state, useStrictElementCheck))
.then(state => {
return skipMoving ? state : this._wrapAction(() => this._moveToElement());
})
.then(state => {
if (!skipCheckAfterMoving)
VisibleElementAutomation._checkElementState(state, useStrictElementCheck);
this._checkElementState(state, useStrictElementCheck);

return state;
})
Expand Down
7 changes: 7 additions & 0 deletions src/client/core/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import * as pageUnloadBarrier from './barriers/page-unload-barrier';
import { preventRealEvents, disableRealEventsPreventing } from './prevent-real-events';
import scrollController from './scroll/controller';
import ScrollAutomation from './scroll/index';
import selectController from './select';

import * as serviceUtils from './utils/service';
import * as domUtils from './utils/dom';
Expand All @@ -35,6 +36,8 @@ import * as browser from '../browser';
import selectorTextFilter from '../../client-functions/selectors/selector-text-filter';
import selectorAttributeFilter from '../../client-functions/selectors/selector-attribute-filter';

import { TEST_RUN_ERRORS, RUNTIME_ERRORS } from '../../errors/types';

const exports = {};

exports.RequestBarrier = RequestBarrier;
Expand All @@ -46,6 +49,7 @@ exports.preventRealEvents = preventRealEvents;
exports.disableRealEventsPreventing = disableRealEventsPreventing;
exports.scrollController = scrollController;
exports.ScrollAutomation = ScrollAutomation;
exports.selectController = selectController;

exports.serviceUtils = serviceUtils;
exports.domUtils = domUtils;
Expand All @@ -72,6 +76,9 @@ exports.stringifyElement = stringifyElement;
exports.selectorTextFilter = selectorTextFilter;
exports.selectorAttributeFilter = selectorAttributeFilter;

exports.TEST_RUN_ERRORS = TEST_RUN_ERRORS;
exports.RUNTIME_ERRORS = RUNTIME_ERRORS;

const nativeMethods = hammerhead.nativeMethods;
const evalIframeScript = hammerhead.EVENTS.evalIframeScript;

Expand Down
112 changes: 112 additions & 0 deletions src/client/core/select/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import hammerhead from '../deps/hammerhead';
import * as domUtils from '../utils/dom';
import * as styleUtils from '../utils/style';

const shadowUI = hammerhead.shadowUI;
const nativeMethods = hammerhead.nativeMethods;

const OPTION_GROUP_CLASS = 'tcOptionGroup';
const OPTION_CLASS = 'tcOption';
const DISABLED_CLASS = 'disabled';


class SelectController {
public currentEl: HTMLSelectElement | null;
public optionList: HTMLElement | null;
public groups: HTMLElement[];
public options: HTMLElement[];

constructor () {
this.currentEl = null;
this.optionList = null;
this.groups = [];
this.options = [];
}

_createOption (realOption: HTMLOptionElement, parent: HTMLElement): void {
const option = document.createElement('div');
const isOptionDisabled = realOption.disabled || domUtils.getTagName(realOption.parentElement) === 'optgroup' &&
(realOption.parentElement as HTMLOptGroupElement).disabled;

// eslint-disable-next-line no-restricted-properties
const text = domUtils.getTagName(realOption) === 'option' ? (realOption as HTMLOptionElement).text : '';

nativeMethods.nodeTextContentSetter.call(option, text);

parent.appendChild(option);
shadowUI.addClass(option, OPTION_CLASS);

if (isOptionDisabled) {
shadowUI.addClass(option, DISABLED_CLASS);
styleUtils.set(option, 'color', styleUtils.get(realOption, 'color'));
}

this.options.push(option);
}

_createGroup (realGroup: HTMLOptGroupElement, parent: HTMLElement): void {
const group = document.createElement('div');

nativeMethods.nodeTextContentSetter.call(group, realGroup.label || ' ');
parent.appendChild(group);

shadowUI.addClass(group, OPTION_GROUP_CLASS);

if (realGroup.disabled) {
shadowUI.addClass(group, DISABLED_CLASS);

styleUtils.set(group, 'color', styleUtils.get(realGroup, 'color'));
}

this.createChildren(realGroup.children, group);

this.groups.push(group);
}

clear (): void {
this.optionList = null;
this.currentEl = null;
this.options = [];
this.groups = [];
}

createChildren (children: HTMLCollection, parent: HTMLElement): void {
const childrenLength = domUtils.getChildrenLength(children);

for (let i = 0; i < childrenLength; i++) {
if (domUtils.isOptionElement(children[i]))
this._createOption(children[i] as HTMLOptionElement, parent);
else if (domUtils.getTagName(children[i]) === 'optgroup')
this._createGroup(children[i] as HTMLOptGroupElement, parent);
}
}

getEmulatedChildElement (element: HTMLElement): HTMLElement {
const isGroup = domUtils.getTagName(element) === 'optgroup';
const elementIndex = isGroup ? domUtils.getElementIndexInParent(this.currentEl, element) :
domUtils.getElementIndexInParent(this.currentEl, element);

if (!isGroup)
return this.options[elementIndex];

return this.groups[elementIndex];
}

isOptionListExpanded (select: HTMLSelectElement): boolean {
return select ? select === this.currentEl : !!this.currentEl;
}

isOptionElementVisible (el: HTMLElement): boolean {
const parentSelect = domUtils.getSelectParent(el);

if (!parentSelect)
return true;

const expanded = this.isOptionListExpanded(parentSelect);
const selectSizeValue = styleUtils.getSelectElementSize(parentSelect);

return expanded || selectSizeValue > 1;
}
}

export default new SelectController();
Loading

0 comments on commit 38e2150

Please sign in to comment.