Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixing FindRegion + Refactor #1408

Merged
merged 15 commits into from
Nov 24, 2023
22 changes: 22 additions & 0 deletions packages/webdriver-utils/src/driver.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,4 +77,26 @@ export default class Driver {
const response = JSON.parse((await request(baseUrl, options)).body);
return response.value;
}

async findElementBoundingBox(using, value) {
if (using === 'xpath') {
return await this.findElementXpath(value);
} else if (using === 'css selector') {
return await this.findElementSelector(value);
}
}

async findElementXpath(xpath) {
xpath = xpath.replace(/'/g, '"');
const command = { script: `return document.evaluate('${xpath}', document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue.getBoundingClientRect();`, args: [] };
const response = await this.executeScript(command);
return response.value;
}

async findElementSelector(selector) {
selector = selector.replace('\\', '\\\\');
const command = { script: `return document.querySelector('${selector}').getBoundingClientRect();`, args: [] };
const response = await this.executeScript(command);
return response.value;
}
}
148 changes: 125 additions & 23 deletions packages/webdriver-utils/src/providers/genericProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ export default class GenericProvider {
this.debugUrl = null;
this.header = 0;
this.footer = 0;
this.statusBarHeight = 0;
this.pageXShiftFactor = 0;
this.pageYShiftFactor = 0;
this.currentTag = null;
this.removeElementShiftFactor = 50000;
Copy link
Contributor

Choose a reason for hiding this comment

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

What's the use of removeElementShiftFactor ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is to make the coordinates incorrect when the element is not visible on screen. So when scroll values don't match the top and bottom becomes negative causing it to be 0 and hence not drawing it.

this.initialScrollFactor = { value: [0, 0] };
}

addDefaultOptions() {
Expand Down Expand Up @@ -63,6 +69,18 @@ export default class GenericProvider {
}
}

async getInitialPosition() {
if (this.currentTag.osName === 'iOS') {
prklm10 marked this conversation as resolved.
Show resolved Hide resolved
this.initialScrollFactor = await this.driver.executeScript({ script: 'return [parseInt(window.scrollX), parseInt(window.scrollY)];', args: [] });
}
}

async scrollToInitialPosition(x, y) {
if (this.currentTag.osName === 'iOS') {
await this.driver.executeScript({ script: `window.scrollTo(${x}, ${y})`, args: [] });
}
}

async screenshot(name, {
ignoreRegionXpaths = [],
ignoreRegionSelectors = [],
Expand All @@ -86,6 +104,9 @@ export default class GenericProvider {
const tiles = await this.getTiles(this.header, this.footer, fullscreen);
log.debug(`[${name}] : Tiles ${JSON.stringify(tiles)}`);

this.currentTag = tag;
this.statusBarHeight = tiles.tiles[0].statusBarHeight;

const ignoreRegions = await this.findRegions(
ignoreRegionXpaths, ignoreRegionSelectors, ignoreRegionElements, customIgnoreRegions
);
Expand Down Expand Up @@ -172,30 +193,67 @@ export default class GenericProvider {
this.debugUrl = 'https://localhost/v1';
}

async doTransformations() {
const hideScrollbarStyle = `
/* Hide scrollbar for Chrome, Safari and Opera */
::-webkit-scrollbar {
display: none !important;
}

/* Hide scrollbar for IE, Edge and Firefox */
body, html {
-ms-overflow-style: none !important; /* IE and Edge */
scrollbar-width: none !important; /* Firefox */
}`.replace(/\n/g, '');
const jsScript = `
const e = document.createElement('style');
e.setAttribute('class', 'poa-injected');
e.innerHTML = '${hideScrollbarStyle}'
document.head.appendChild(e);`;

await this.driver.executeScript({ script: jsScript, args: [] });
}

async undoTransformations(data) {
const jsScript = `
const n = document.querySelectorAll('${data}');
n.forEach((e) => {e.remove()});`;

await this.driver.executeScript({ script: jsScript, args: [] });
}

async findRegions(xpaths, selectors, elements, customLocations) {
const xpathRegions = await this.getSeleniumRegionsBy('xpath', xpaths);
const selectorRegions = await this.getSeleniumRegionsBy('css selector', selectors);
const elementRegions = await this.getSeleniumRegionsByElement(elements);
const customRegions = await this.getSeleniumRegionsByLocation(customLocations);

return [
...xpathRegions,
...selectorRegions,
...elementRegions,
...customRegions
];
let isRegionPassed = [xpaths, selectors, elements, customLocations].some(regions => regions.length > 0);
if (isRegionPassed) {
await this.doTransformations();
const xpathRegions = await this.getSeleniumRegionsBy('xpath', xpaths);
const selectorRegions = await this.getSeleniumRegionsBy('css selector', selectors);
const elementRegions = await this.getSeleniumRegionsByElement(elements);
const customRegions = await this.getSeleniumRegionsByLocation(customLocations);
await this.undoTransformations('.poa-injected');
nilshah98 marked this conversation as resolved.
Show resolved Hide resolved

return [
...xpathRegions,
...selectorRegions,
...elementRegions,
...customRegions
];
} else {
return [];
}
}

async getRegionObject(selector, elementId) {
const scaleFactor = parseInt(await this.metaData.devicePixelRatio());
const rect = await this.driver.rect(elementId);
const location = { x: rect.x, y: rect.y };
const size = { height: rect.height, width: rect.width };
async getRegionObjectFromBoundingBox(selector, element) {
const scaleFactor = await this.metaData.devicePixelRatio();
let headerAdjustment = 0;
if (this.currentTag.osName === 'iOS') {
headerAdjustment = this.statusBarHeight;
nilshah98 marked this conversation as resolved.
Show resolved Hide resolved
}
const coOrdinates = {
top: Math.floor(location.y * scaleFactor),
bottom: Math.ceil((location.y + size.height) * scaleFactor),
left: Math.floor(location.x * scaleFactor),
right: Math.ceil((location.x + size.width) * scaleFactor)
top: Math.floor(element.y * scaleFactor) + Math.floor(headerAdjustment),
bottom: Math.ceil((element.y + element.height) * scaleFactor) + Math.ceil(headerAdjustment),
left: Math.floor(element.x * scaleFactor),
right: Math.ceil((element.x + element.width) * scaleFactor)
};

const jsonObject = {
Expand All @@ -210,9 +268,9 @@ export default class GenericProvider {
const regionsArray = [];
for (const idx in elements) {
try {
const element = await this.driver.findElement(findBy, elements[idx]);
const boundingBoxRegion = await this.driver.findElementBoundingBox(findBy, elements[idx]);
const selector = `${findBy}: ${elements[idx]}`;
const region = await this.getRegionObject(selector, element[Object.keys(element)[0]]);
const region = await this.getRegionObjectFromBoundingBox(selector, boundingBoxRegion);
regionsArray.push(region);
} catch (e) {
log.warn(`Selenium Element with ${findBy}: ${elements[idx]} not found. Ignoring this ${findBy}.`);
Expand All @@ -222,19 +280,63 @@ export default class GenericProvider {
return regionsArray;
}

async updatePageShiftFactor(location, scaleFactor) {
const scrollFactors = await this.driver.executeScript({ script: 'return [parseInt(window.scrollX), parseInt(window.scrollY)];', args: [] });
if (this.currentTag.osName === 'iOS' || (this.currentTag.osName === 'OS X' && parseInt(this.currentTag.browserVersion) > 13 && this.currentTag.browserName.toLowerCase() === 'safari')) {
this.pageYShiftFactor = this.statusBarHeight;
} else {
this.pageYShiftFactor = this.statusBarHeight - (scrollFactors.value[1] * scaleFactor);
}
this.pageXShiftFactor = this.currentTag.osName === 'iOS' ? 0 : (-(scrollFactors.value[0] * scaleFactor));
if (this.currentTag.osName === 'iOS') {
if (scrollFactors.value[0] !== this.initialScrollFactor.value[0] || scrollFactors.value[1] !== this.initialScrollFactor.value[1]) {
this.pageXShiftFactor = (-1 * this.removeElementShiftFactor);
this.pageYShiftFactor = (-1 * this.removeElementShiftFactor);
} else if (location.y === 0) {
this.pageYShiftFactor += (-(scrollFactors.value[1] * scaleFactor));
}
}
}

async getRegionObject(selector, elementId) {
const scaleFactor = await this.metaData.devicePixelRatio();
const rect = await this.driver.rect(elementId);
const location = { x: rect.x, y: rect.y };
const size = { height: rect.height, width: rect.width };
// Update pageShiftFactor Element is not visible in viewport
// In case of iOS if the element is not visible in viewport it gives 0 for x-y coordinate.
// In case of iOS if the element is partially visible it gives negative x-y coordinate.
// Subtracting ScrollY/ScrollX ensures if the element is visible in viewport or not.
await this.updatePageShiftFactor(location, scaleFactor);
const coOrdinates = {
top: Math.floor(location.y * scaleFactor) + Math.floor(this.pageYShiftFactor),
bottom: Math.ceil((location.y + size.height) * scaleFactor) + Math.ceil(this.pageYShiftFactor),
left: Math.floor(location.x * scaleFactor) + Math.floor(this.pageXShiftFactor),
right: Math.ceil((location.x + size.width) * scaleFactor) + Math.ceil(this.pageXShiftFactor)
};

const jsonObject = {
selector,
coOrdinates
};

return jsonObject;
}

async getSeleniumRegionsByElement(elements) {
const regionsArray = [];
await this.getInitialPosition();
for (let index = 0; index < elements.length; index++) {
try {
const selector = `element: ${index}`;

const region = await this.getRegionObject(selector, elements[index]);
regionsArray.push(region);
} catch (e) {
log.warn(`Correct Web Element not passed at index ${index}.`);
log.debug(e.toString());
}
}
await this.scrollToInitialPosition(this.initialScrollFactor.value[0], this.initialScrollFactor.value[1]);
return regionsArray;
}

Expand Down
60 changes: 60 additions & 0 deletions packages/webdriver-utils/test/driver.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,66 @@ describe('Driver', () => {
});
});

describe('findElementBoundingBox', () => {
let xpathFindElementSpy;
let cssSelectorFindElementSpy;
beforeEach(() => {
xpathFindElementSpy = spyOn(Driver.prototype, 'findElementXpath').and.returnValue(Promise.resolve({ x: 0, y: 10, height: 100, width: 100 }));
cssSelectorFindElementSpy = spyOn(Driver.prototype, 'findElementSelector').and.returnValue(Promise.resolve({ x: 0, y: 10, height: 100, width: 100 }));
});
describe('when xpath is passed', () => {
it('calls the required function', async () => {
const res = await driver.findElementBoundingBox('xpath', '/xpath1');
expect(cssSelectorFindElementSpy).toHaveBeenCalledTimes(0);
expect(xpathFindElementSpy).toHaveBeenCalledTimes(1);
expect(xpathFindElementSpy).toHaveBeenCalledWith('/xpath1');
expect(res).toEqual({ x: 0, y: 10, height: 100, width: 100 });
});
});

describe('when selector is passed', () => {
it('calls the required function', async () => {
const res = await driver.findElementBoundingBox('css selector', '#id1');
expect(xpathFindElementSpy).toHaveBeenCalledTimes(0);
expect(cssSelectorFindElementSpy).toHaveBeenCalledTimes(1);
expect(cssSelectorFindElementSpy).toHaveBeenCalledWith('#id1');
expect(res).toEqual({ x: 0, y: 10, height: 100, width: 100 });
});
});

describe('when invalid is passed', () => {
it('calls nothing', async () => {
await driver.findElementBoundingBox('abc', '#id1');
expect(xpathFindElementSpy).toHaveBeenCalledTimes(0);
expect(cssSelectorFindElementSpy).toHaveBeenCalledTimes(0);
});
});
});

describe('findElementXpath', () => {
let executeScriptSpy;
beforeEach(() => {
executeScriptSpy = spyOn(Driver.prototype, 'executeScript').and.returnValue(Promise.resolve({ value: { x: 0, y: 10, height: 100, width: 100 } }));
});
it('calls requests', async () => {
const res = await driver.findElementXpath('/xpath1');
expect(executeScriptSpy).toHaveBeenCalledWith({ script: "return document.evaluate('/xpath1', document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue.getBoundingClientRect();", args: [] });
expect(res).toEqual({ x: 0, y: 10, height: 100, width: 100 });
});
});

describe('findElementSelector', () => {
let executeScriptSpy;
beforeEach(() => {
executeScriptSpy = spyOn(Driver.prototype, 'executeScript').and.returnValue(Promise.resolve({ value: { x: 0, y: 10, height: 100, width: 100 } }));
});
it('calls requests', async () => {
const res = await driver.findElementSelector('#id1');
expect(executeScriptSpy).toHaveBeenCalledWith({ script: "return document.querySelector('#id1').getBoundingClientRect();", args: [] });
expect(res).toEqual({ x: 0, y: 10, height: 100, width: 100 });
});
});

describe('rect', () => {
it('calls requests', async () => {
const elementId = 'element';
Expand Down
Loading