Skip to content
This repository has been archived by the owner on Apr 25, 2023. It is now read-only.

Highlight elements that user searched for #344

Merged
merged 6 commits into from
Nov 21, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 35 additions & 34 deletions app/main/appium-method-handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,19 +58,42 @@ export default class AppiumMethodHandler {
return {variableName, variableType, strategy, selector, elements};
}

async executeElementCommand (elementId, methodName, args = []) {
const cachedEl = this.elementCache[elementId];
async _execute ({elementId, methodName, args, skipScreenshotAndSource}) {
let cachedEl;
let res = {};

// Give the cached element a variable name (el1, el2, el3,...) the first time it's used
if (!cachedEl.variableName && cachedEl.variableType === 'string') {
cachedEl.variableName = `el${this.elVariableCounter++}`;
if (elementId) {
// Give the cached element a variable name (el1, el2, el3,...) the first time it's used
cachedEl = this.elementCache[elementId];
if (!cachedEl.variableName && cachedEl.variableType === 'string') {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can use _.isString

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variableType is a hard-coded string that identifies what type the element is. It's used by the RecordedActions component which displays recorded code and it uses the variableType to determine if it should be el.text("String") or el.text(123) etc...

cachedEl.variableName = `el${this.elVariableCounter++}`;
}
res = await cachedEl.el[methodName].apply(cachedEl.el, args);
} else {
// Specially handle the tap and swipe method
if (methodName === 'tap') {
res = await (new wd.TouchAction(this.driver))
.tap({x: args[0], y: args[1]})
.perform();
} else if (methodName === 'swipe') {
const [startX, startY, endX, endY] = args;
res = await (new wd.TouchAction(this.driver))
.press({x: startX, y: startY})
.moveTo({x: endX, y: endY})
.release()
.perform();
} else if (methodName !== 'source' && methodName !== 'screenshot') {
res = await this.driver[methodName].apply(this.driver, args);
}
}
const res = await cachedEl.el[methodName].apply(cachedEl.el, args);

// Give the source/screenshot time to change before taking the screenshot
await Bluebird.delay(500);

let sourceAndScreenshot = await this._getSourceAndScreenshot();
let sourceAndScreenshot;
Copy link
Contributor

@mykola-mokhnach mykola-mokhnach Nov 15, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume an empty array should be assigned to this variable by default since we apply ellipsis operator to it later

Copy link
Contributor Author

@dpgraham dpgraham Nov 16, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When you pass undefined or null into an object via ellipsis (spread) operator, it has no effect.

if (!skipScreenshotAndSource) {
sourceAndScreenshot = await this._getSourceAndScreenshot();
}

return {
...sourceAndScreenshot,
Expand All @@ -79,34 +102,12 @@ export default class AppiumMethodHandler {
};
}

async executeMethod (methodName, args = []) {
let res = {};

// Specially handle the tap and swipe method
if (methodName === 'tap') {
res = await (new wd.TouchAction(this.driver))
.tap({x: args[0], y: args[1]})
.perform();
} else if (methodName === 'swipe') {
const [startX, startY, endX, endY] = args;
res = await (new wd.TouchAction(this.driver))
.press({x: startX, y: startY})
.moveTo({x: endX, y: endY})
.release()
.perform();
} else if (methodName !== 'source' && methodName !== 'screenshot') {
res = await this.driver[methodName].apply(this.driver, args);
}

// Give the source/screenshot time to change before taking the screenshot
await Bluebird.delay(500);

let sourceAndScreenshot = await this._getSourceAndScreenshot();
async executeElementCommand (elementId, methodName, args = [], skipScreenshotAndSource = false) {
return await this._execute({elementId, methodName, args, skipScreenshotAndSource});
}

return {
...sourceAndScreenshot,
res,
};
async executeMethod (methodName, args = [], skipScreenshotAndSource = false) {
return await this._execute({methodName, args, skipScreenshotAndSource});
}

async _getSourceAndScreenshot () {
Expand Down
7 changes: 4 additions & 3 deletions app/main/appium.js
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,7 @@ function connectClientMethodListener () {
fetchArray = false, // Optional. Are we fetching an array of elements or just one?
elementId, // Optional. Element being operated on
args = [], // Optional. Arguments passed to method
skipScreenshotAndSource = false, // Optional. Do we want the updated source and screenshot?
} = data;

let renderer = evt.sender;
Expand All @@ -322,15 +323,15 @@ function connectClientMethodListener () {
if (methodName) {
if (elementId) {
console.log(`Handling client method request with method '${methodName}', args ${JSON.stringify(args)} and elementId ${elementId}`);
res = await methodHandler.executeElementCommand(elementId, methodName, args);
res = await methodHandler.executeElementCommand(elementId, methodName, args, skipScreenshotAndSource);
} else {
console.log(`Handling client method request with method '${methodName}' and args ${JSON.stringify(args)}`);
res = await methodHandler.executeMethod(methodName, args);
res = await methodHandler.executeMethod(methodName, args, skipScreenshotAndSource);
}
} else if (strategy && selector) {
if (fetchArray) {
console.log(`Fetching elements with selector '${selector}' and strategy ${strategy}`);
res = await methodHandler.fetchElements(strategy, selector);
res = await methodHandler.fetchElements(strategy, selector, skipScreenshotAndSource);
} else {
console.log(`Fetching an element with selector '${selector}' and strategy ${strategy}`);
res = await methodHandler.fetchElement(strategy, selector);
Expand Down
19 changes: 16 additions & 3 deletions app/renderer/actions/Inspector.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { ipcRenderer } from 'electron';
import { notification } from 'antd';
import { push } from 'react-router-redux';
import _ from 'lodash';
import B from 'bluebird';
import { getLocators } from '../components/Inspector/shared';
import { showError } from './Session';
import { callClientMethod } from './shared';
Expand Down Expand Up @@ -46,11 +47,14 @@ export const CLEAR_SEARCH_RESULTS = 'CLEAR_SEARCH_RESULTS';
export const ADD_ASSIGNED_VAR_CACHE = 'ADD_ASSIGNED_VAR_CACHE';
export const CLEAR_ASSIGNED_VAR_CACHE = 'CLEAR_ASSIGNED_VAR_CACHE';
export const SET_SCREENSHOT_INTERACTION_MODE = 'SET_SCREENSHOT_INTERACTION_MODE';
export const SET_SEARCHED_FOR_ELEMENT_BOUNDS = 'SET_SEARCHED_FOR_ELEMENT_BOUNDS';
export const CLEAR_SEARCHED_FOR_ELEMENT_BOUNDS = 'CLEAR_SEARCHED_FOR_ELEMENT_BOUNDS';

export const SET_SWIPE_START = 'SET_SWIPE_START';
export const SET_SWIPE_END = 'SET_SWIPE_END';
export const CLEAR_SWIPE_ACTION = 'CLEAR_SWIPE_ACTION';


// Attributes on nodes that we know are unique to the node
const uniqueAttributes = [
'name',
Expand Down Expand Up @@ -335,6 +339,7 @@ export function showLocatorTestModal () {
export function hideLocatorTestModal () {
return (dispatch) => {
dispatch({type: HIDE_LOCATOR_TEST_MODAL});
dispatch({type: CLEAR_SEARCHED_FOR_ELEMENT_BOUNDS});
};
}

Expand Down Expand Up @@ -377,11 +382,19 @@ export function findAndAssign (strategy, selector, variableName, isArray) {
};
}


// TODO: Is this obsolete? I don't think we use this.
export function setLocatorTestElement (elementId) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't this function be also defined as async?

Copy link
Contributor Author

@dpgraham dpgraham Nov 16, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function that is returned within it is async.

return (dispatch) => {
return async (dispatch) => {
dispatch({type: SET_LOCATOR_TEST_ELEMENT, elementId});
dispatch({type: CLEAR_SEARCHED_FOR_ELEMENT_BOUNDS});
if (elementId) {
try {
const [location, size] = await(B.all([
callClientMethod({methodName: 'getLocation', args: [elementId], skipScreenshotAndSource: true, skipRecord: true}),
callClientMethod({methodName: 'getSize', args: [elementId], skipScreenshotAndSource: true, skipRecord: true}),
]));
dispatch({type: SET_SEARCHED_FOR_ELEMENT_BOUNDS, location: location.res, size: size.res});
} catch (ign) { }
}
};
}

Expand Down
45 changes: 29 additions & 16 deletions app/renderer/components/Inspector/HighlighterRect.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,31 +10,44 @@ import { parseCoordinates } from './shared';
export default class HighlighterRect extends Component {

render () {
const {selectedElement = {}, selectHoveredElement, unselectHoveredElement, hoveredElement = {}, selectElement, unselectElement, element, zIndex, scaleRatio, xOffset} = this.props;
const {selectedElement = {}, selectHoveredElement, unselectHoveredElement, hoveredElement = {}, selectElement, unselectElement, element,
zIndex, scaleRatio, xOffset, elLocation, elSize} = this.props;
const {path: hoveredPath} = hoveredElement;
const {path: selectedPath} = selectedElement;

// Calculate left, top, width and height coordinates
const {x1, y1, x2, y2} = parseCoordinates(element);
const left = x1 / scaleRatio + xOffset;
const top = y1 / scaleRatio;
const width = (x2 - x1) / scaleRatio;
const height = (y2 - y1) / scaleRatio;
let width, height, left, top, highlighterClasses, key;
highlighterClasses = [InspectorCSS['highlighter-box']];

// Add class + special classes to hovered and selected elements
const highlighterClasses = [InspectorCSS['highlighter-box']];
if (hoveredPath === element.path) {
highlighterClasses.push(InspectorCSS['hovered-element-box']);
}
if (selectedPath === element.path) {
if (element) {
// Calculate left, top, width and height coordinates
const {x1, y1, x2, y2} = parseCoordinates(element);
left = x1 / scaleRatio + xOffset;
top = y1 / scaleRatio;
width = (x2 - x1) / scaleRatio;
height = (y2 - y1) / scaleRatio;

// Add class + special classes to hovered and selected elements
if (hoveredPath === element.path) {
highlighterClasses.push(InspectorCSS['hovered-element-box']);
}
if (selectedPath === element.path) {
highlighterClasses.push(InspectorCSS['inspected-element-box']);
}
key = element.path;
} else if (elLocation && elSize) {
width = elSize.width / scaleRatio;
height = elSize.height / scaleRatio;
top = elLocation.y / scaleRatio;
left = elLocation.x / scaleRatio + xOffset;
key = 'searchedForElement';
highlighterClasses.push(InspectorCSS['inspected-element-box']);
}

return <div className={highlighterClasses.join(' ').trim()}
onMouseOver={() => selectHoveredElement(element.path)}
onMouseOver={() => selectHoveredElement(key)}
onMouseOut={unselectHoveredElement}
onClick={() => element.path === selectedPath ? unselectElement() : selectElement(element.path)}
key={element.path}
onClick={() => key === selectedPath ? unselectElement() : selectElement(key)}
key={key}
style={{zIndex, left: (left || 0), top: (top || 0), width: (width || 0), height: (height || 0)}}>
<div></div>
</div>;
Expand Down
14 changes: 11 additions & 3 deletions app/renderer/components/Inspector/HighlighterRects.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ export default class HighlighterRects extends Component {
}

render () {
const {source, screenshotInteractionMode, containerEl} = this.props;
const {source, screenshotInteractionMode, containerEl, searchedForElementBounds, isLocatorTestModalVisible} = this.props;
const {scaleRatio} = this.state;

// Recurse through the 'source' JSON and render a highlighter rect for each element
Expand All @@ -111,7 +111,6 @@ export default class HighlighterRects extends Component {
containerEl.getBoundingClientRect().left;
}

// TODO: Refactor this into a separate component
let recursive = (element, zIndex = 0) => {
if (!element) {
return;
Expand All @@ -129,13 +128,22 @@ export default class HighlighterRects extends Component {
}
};

// If the use selected an element that they searched for, highlight that element
if (searchedForElementBounds && isLocatorTestModalVisible) {
const {location:elLocation, size} = searchedForElementBounds;
highlighterRects.push(<HighlighterRect elSize={size} elLocation={elLocation} scaleRatio={scaleRatio} xOffset={highlighterXOffset} />);
}

// If we're tapping or swiping, show the 'crosshair' cursor style
const screenshotStyle = {};
if (screenshotInteractionMode === 'tap' || screenshotInteractionMode === 'swipe') {
screenshotStyle.cursor = 'crosshair';
}

recursive(source);
// Don't show highlighter rects when Search Elements modal is open
if (!isLocatorTestModalVisible) {
recursive(source);
}

return <div>{ highlighterRects }</div>;
}
Expand Down
2 changes: 2 additions & 0 deletions app/renderer/components/Inspector/Screenshot.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ export default class Screenshot extends Component {
this.containerEl = null;
this.state = {
scaleRatio: 1,
x: null,
y: null,
};
this.updateScaleRatio = debounce(this.updateScaleRatio.bind(this), 1000);
}
Expand Down
18 changes: 17 additions & 1 deletion app/renderer/reducers/Inspector.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { SET_SOURCE_AND_SCREENSHOT, QUIT_SESSION_REQUESTED, QUIT_SESSION_DONE,
SHOW_LOCATOR_TEST_MODAL, HIDE_LOCATOR_TEST_MODAL, SET_LOCATOR_TEST_STRATEGY, SET_LOCATOR_TEST_VALUE,
SEARCHING_FOR_ELEMENTS, SEARCHING_FOR_ELEMENTS_COMPLETED, SET_LOCATOR_TEST_ELEMENT, CLEAR_SEARCH_RESULTS,
ADD_ASSIGNED_VAR_CACHE, CLEAR_ASSIGNED_VAR_CACHE, SET_SCREENSHOT_INTERACTION_MODE,
SET_SWIPE_START, SET_SWIPE_END, CLEAR_SWIPE_ACTION
SET_SWIPE_START, SET_SWIPE_END, CLEAR_SWIPE_ACTION, SET_SEARCHED_FOR_ELEMENT_BOUNDS, CLEAR_SEARCHED_FOR_ELEMENT_BOUNDS,
} from '../actions/Inspector';

const DEFAULT_FRAMEWORK = 'java';
Expand All @@ -28,6 +28,7 @@ const INITIAL_STATE = {
isSearchingForElements: false,
assignedVarCache: {},
screenshotInteractionMode: 'select',
searchedForElementBounds: null,
};

/**
Expand Down Expand Up @@ -287,6 +288,21 @@ export default function inspector (state=INITIAL_STATE, action) {
swipeEnd: null,
};

case SET_SEARCHED_FOR_ELEMENT_BOUNDS:
return {
...state,
searchedForElementBounds: {
location: action.location,
size: action.size,
}
};

case CLEAR_SEARCHED_FOR_ELEMENT_BOUNDS:
return {
...state,
searchedForElementBounds: null,
};

default:
return {...state};
}
Expand Down