@qawolf/web
Advanced tools
Comparing version 0.5.7 to 0.5.8
@@ -9,1 +9,3 @@ import { ElementDescriptor } from "@qawolf/types"; | ||
export declare const getDescriptor: (element: HTMLElement, dataAttribute: string | null) => ElementDescriptor; | ||
export declare const isClickable: (element: HTMLElement, computedStyle: CSSStyleDeclaration) => boolean; | ||
export declare const isVisible: (element: HTMLElement, computedStyle?: CSSStyleDeclaration | undefined) => boolean; |
@@ -133,2 +133,13 @@ "use strict"; | ||
}; | ||
exports.isClickable = (element, computedStyle) => { | ||
const clickable = computedStyle.cursor === "pointer"; | ||
return clickable && exports.isVisible(element, computedStyle); | ||
}; | ||
exports.isVisible = (element, computedStyle) => { | ||
if (element.offsetWidth <= 0 || element.offsetHeight <= 0) | ||
return false; | ||
if (computedStyle && computedStyle.visibility === "hidden") | ||
return false; | ||
return true; | ||
}; | ||
//# sourceMappingURL=element.js.map |
@@ -7,8 +7,8 @@ import { Action, Locator } from "@qawolf/types"; | ||
}; | ||
export declare const isVisible: (element: HTMLElement) => boolean; | ||
export declare const queryActionElements: (action: Action) => HTMLElement[]; | ||
export declare const queryDataElements: ({ action, dataAttribute, dataValue }: QueryByDataArgs) => HTMLElement[]; | ||
export declare const queryVisibleElements: (selector: string) => HTMLElement[]; | ||
export declare const findClickableAncestor: (element: HTMLElement, dataAttribute: string) => HTMLElement; | ||
export declare const findElement: ({ action, dataAttribute, target, timeoutMs, value }: Locator) => Promise<HTMLElement | null>; | ||
export declare const hasText: (text: string, timeoutMs: number) => Promise<boolean>; | ||
export {}; |
@@ -12,8 +12,6 @@ "use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const element_1 = require("./element"); | ||
const match_1 = require("./match"); | ||
const wait_1 = require("./wait"); | ||
const xpath_1 = require("./xpath"); | ||
exports.isVisible = (element) => { | ||
return !!(element.offsetWidth && element.offsetHeight); | ||
}; | ||
exports.queryActionElements = (action) => { | ||
@@ -35,3 +33,3 @@ const selector = action === "type" ? "input,select,textarea,[contenteditable='true']" : "*"; | ||
for (let i = 0; i < elements.length; i++) { | ||
if (exports.isVisible(elements[i])) { | ||
if (element_1.isVisible(elements[i])) { | ||
visibleElements.push(elements[i]); | ||
@@ -42,2 +40,19 @@ } | ||
}; | ||
exports.findClickableAncestor = (element, dataAttribute) => { | ||
let ancestor = element; | ||
console.log("find clickable ancestor for", xpath_1.getXpath(element)); | ||
while (ancestor.parentElement) { | ||
const dataValue = element_1.getDataValue(ancestor, dataAttribute); | ||
if (dataValue) { | ||
console.log(`found clickable ancestor with ${dataAttribute}="${dataValue}"`, xpath_1.getXpath(ancestor)); | ||
return ancestor; | ||
} | ||
if (!element_1.isClickable(ancestor.parentElement, window.getComputedStyle(ancestor.parentElement))) { | ||
console.log("found clickable ancestor", xpath_1.getXpath(ancestor)); | ||
return ancestor; | ||
} | ||
ancestor = ancestor.parentElement; | ||
} | ||
return ancestor; | ||
}; | ||
exports.findElement = ({ action, dataAttribute, target, timeoutMs, value }) => __awaiter(void 0, void 0, void 0, function* () { | ||
@@ -60,4 +75,4 @@ if (dataAttribute && target.dataValue) { | ||
return xpath_1.findElementByXpath(target.xpath); | ||
console.log("waiting for top strong match", target); | ||
const strongMatch = yield wait_1.waitFor(() => { | ||
console.log("waiting for top strong match", target); | ||
const elements = exports.queryActionElements(action); | ||
@@ -64,0 +79,0 @@ return match_1.topMatch({ |
@@ -19,3 +19,3 @@ import * as element from "./element"; | ||
}[], countPresentKeys: (descriptor: import("@qawolf/types").ElementDescriptor) => number, isNil: (value?: any) => boolean; | ||
declare const sleep: (milliseconds: number) => Promise<void>, waitFor: <T>(valueFn: () => T | null, timeoutMs: number, sleepMs?: number) => Promise<T | null>, waitUntil: (booleanFn: () => boolean, timeoutMs: number, sleepMs?: number) => Promise<void>; | ||
declare const sleep: (milliseconds: number) => Promise<void>, waitFor: <T>(valueFn: () => T | null, timeoutMs: number, sleepMs?: number) => Promise<T | null>, waitUntil: (booleanFn: () => boolean | Promise<boolean>, timeoutMs: number, sleepMs?: number) => Promise<void>; | ||
export { compareArrays, compareDescriptorKey, compareDescriptors, countPresentKeys, isKeyEvent, isNil, sleep, waitFor, waitUntil }; | ||
@@ -22,0 +22,0 @@ declare const webExports: { |
@@ -23,4 +23,4 @@ import { ElementDescriptor } from "@qawolf/types"; | ||
export declare const compareArrays: (base?: string[] | null | undefined, compare?: string[] | null | undefined) => number; | ||
export declare const compareDescriptorKey: (key: "dataValue" | "title" | "alt" | "ariaLabel" | "classList" | "href" | "iconContent" | "id" | "inputType" | "isContentEditable" | "labels" | "name" | "parentText" | "placeholder" | "src" | "tagName" | "innerText" | "xpath", targetValue: DescriptorValue, compareValue: DescriptorValue) => { | ||
key: "dataValue" | "title" | "alt" | "ariaLabel" | "classList" | "href" | "iconContent" | "id" | "inputType" | "isContentEditable" | "labels" | "name" | "parentText" | "placeholder" | "src" | "tagName" | "innerText" | "xpath"; | ||
export declare const compareDescriptorKey: (key: "alt" | "ariaLabel" | "classList" | "dataValue" | "href" | "iconContent" | "id" | "inputType" | "isContentEditable" | "labels" | "name" | "parentText" | "placeholder" | "src" | "tagName" | "innerText" | "title" | "xpath", targetValue: DescriptorValue, compareValue: DescriptorValue) => { | ||
key: "alt" | "ariaLabel" | "classList" | "dataValue" | "href" | "iconContent" | "id" | "inputType" | "isContentEditable" | "labels" | "name" | "parentText" | "placeholder" | "src" | "tagName" | "innerText" | "title" | "xpath"; | ||
percent: number; | ||
@@ -27,0 +27,0 @@ }; |
@@ -187,2 +187,14 @@ var qawolf = (function (exports) { | ||
}; | ||
var isClickable = function (element, computedStyle) { | ||
// assume it is clickable if the cursor is a pointer | ||
var clickable = computedStyle.cursor === "pointer"; | ||
return clickable && isVisible(element, computedStyle); | ||
}; | ||
var isVisible = function (element, computedStyle) { | ||
if (element.offsetWidth <= 0 || element.offsetHeight <= 0) | ||
return false; | ||
if (computedStyle && computedStyle.visibility === "hidden") | ||
return false; | ||
return true; | ||
}; | ||
@@ -197,3 +209,5 @@ var element = /*#__PURE__*/Object.freeze({ | ||
getTextContent: getTextContent, | ||
getDescriptor: getDescriptor | ||
getDescriptor: getDescriptor, | ||
isClickable: isClickable, | ||
isVisible: isVisible | ||
}); | ||
@@ -429,11 +443,13 @@ | ||
case 1: | ||
if (!(Date.now() - startTime < timeoutMs)) return [3 /*break*/, 3]; | ||
conditionMet = booleanFn(); | ||
if (!(Date.now() - startTime < timeoutMs)) return [3 /*break*/, 4]; | ||
return [4 /*yield*/, booleanFn()]; | ||
case 2: | ||
conditionMet = _a.sent(); | ||
if (conditionMet) | ||
return [2 /*return*/]; | ||
return [4 /*yield*/, sleep(sleepMs)]; | ||
case 2: | ||
case 3: | ||
_a.sent(); | ||
return [3 /*break*/, 1]; | ||
case 3: throw new Error("waitUntil: waited " + timeoutMs + " milliseconds but condition never met"); | ||
case 4: throw new Error("waitUntil: waited " + timeoutMs + " milliseconds but condition never met"); | ||
} | ||
@@ -451,5 +467,2 @@ }); | ||
var isVisible = function (element) { | ||
return !!(element.offsetWidth && element.offsetHeight); | ||
}; | ||
var queryActionElements = function (action) { | ||
@@ -472,2 +485,4 @@ var selector = action === "type" ? "input,select,textarea,[contenteditable='true']" : "*"; | ||
for (var i = 0; i < elements.length; i++) { | ||
// we do not pass computedStyle because doing | ||
// that for every element would be very expensive | ||
if (isVisible(elements[i])) { | ||
@@ -479,2 +494,24 @@ visibleElements.push(elements[i]); | ||
}; | ||
var findClickableAncestor = function (element, dataAttribute) { | ||
/** | ||
* Crawl up until we reach the top "clickable" ancestor | ||
*/ | ||
var ancestor = element; | ||
console.log("find clickable ancestor for", getXpath(element)); | ||
while (ancestor.parentElement) { | ||
// short-circuit when we encounter a data value | ||
var dataValue = getDataValue(ancestor, dataAttribute); | ||
if (dataValue) { | ||
console.log("found clickable ancestor with " + dataAttribute + "=\"" + dataValue + "\"", getXpath(ancestor)); | ||
return ancestor; | ||
} | ||
// short-circuit when parent is not clickable | ||
if (!isClickable(ancestor.parentElement, window.getComputedStyle(ancestor.parentElement))) { | ||
console.log("found clickable ancestor", getXpath(ancestor)); | ||
return ancestor; | ||
} | ||
ancestor = ancestor.parentElement; | ||
} | ||
return ancestor; | ||
}; | ||
var findElement = function (_a) { | ||
@@ -504,4 +541,4 @@ var action = _a.action, dataAttribute = _a.dataAttribute, target = _a.target, timeoutMs = _a.timeoutMs, value = _a.value; | ||
return [2 /*return*/, findElementByXpath(target.xpath)]; | ||
console.log("waiting for top strong match", target); | ||
return [4 /*yield*/, waitFor(function () { | ||
console.log("waiting for top strong match", target); | ||
var elements = queryActionElements(action); | ||
@@ -554,6 +591,6 @@ return topMatch({ | ||
__proto__: null, | ||
isVisible: isVisible, | ||
queryActionElements: queryActionElements, | ||
queryDataElements: queryDataElements, | ||
queryVisibleElements: queryVisibleElements, | ||
findClickableAncestor: findClickableAncestor, | ||
findElement: findElement, | ||
@@ -594,8 +631,19 @@ hasText: hasText | ||
var _this = this; | ||
this.recordEvent("click", function (event) { return ({ | ||
isTrusted: event.isTrusted, | ||
name: "click", | ||
target: getDescriptor(event.target, _this._dataAttribute), | ||
time: Date.now() | ||
}); }); | ||
this.recordEvent("click", function (event) { | ||
// findClickableAncestor chooses the ancestor if it has a data-attribute | ||
// which is very likely the target we want to click on. | ||
// If there is not a data-attribute on any of the clickable ancestors | ||
// it will take the top most clickable ancestor. | ||
// The ancestor is likely a better target than the descendant. | ||
// Ex. when you click on the i (button > i) or rect (a > svg > rect) | ||
// chances are the ancestor (button, a) is a better target to find. | ||
// XXX if anyone runs into issues with this behavior we can allow disabling it from a flag. | ||
var target = findClickableAncestor(event.target, _this._dataAttribute); | ||
return { | ||
isTrusted: event.isTrusted, | ||
name: "click", | ||
target: getDescriptor(target, _this._dataAttribute), | ||
time: Date.now() | ||
}; | ||
}); | ||
this.recordEvent("input", function (event) { | ||
@@ -602,0 +650,0 @@ var element = event.target; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const element_1 = require("./element"); | ||
const find_1 = require("./find"); | ||
class Recorder { | ||
@@ -31,8 +32,11 @@ constructor(dataAttribute, sendEvent) { | ||
recordEvents() { | ||
this.recordEvent("click", event => ({ | ||
isTrusted: event.isTrusted, | ||
name: "click", | ||
target: element_1.getDescriptor(event.target, this._dataAttribute), | ||
time: Date.now() | ||
})); | ||
this.recordEvent("click", event => { | ||
const target = find_1.findClickableAncestor(event.target, this._dataAttribute); | ||
return { | ||
isTrusted: event.isTrusted, | ||
name: "click", | ||
target: element_1.getDescriptor(target, this._dataAttribute), | ||
time: Date.now() | ||
}; | ||
}); | ||
this.recordEvent("input", event => { | ||
@@ -39,0 +43,0 @@ const element = event.target; |
export declare const sleep: (milliseconds: number) => Promise<void>; | ||
export declare const waitFor: <T>(valueFn: () => T | null, timeoutMs: number, sleepMs?: number) => Promise<T | null>; | ||
export declare const waitUntil: (booleanFn: () => boolean, timeoutMs: number, sleepMs?: number) => Promise<void>; | ||
export declare const waitUntil: (booleanFn: () => boolean | Promise<boolean>, timeoutMs: number, sleepMs?: number) => Promise<void>; |
@@ -28,3 +28,3 @@ "use strict"; | ||
while (Date.now() - startTime < timeoutMs) { | ||
const conditionMet = booleanFn(); | ||
const conditionMet = yield booleanFn(); | ||
if (conditionMet) | ||
@@ -31,0 +31,0 @@ return; |
{ | ||
"name": "@qawolf/web", | ||
"description": "qawolf web library", | ||
"version": "0.5.7", | ||
"version": "0.5.8", | ||
"license": "BSD-3.0", | ||
@@ -33,3 +33,3 @@ "main": "./lib/index.js", | ||
}, | ||
"gitHead": "e9e0ec59744286f261d36a8f82d7059a8a69cd3c" | ||
"gitHead": "7f90e207884baeb510f318b5489226a554bc076a" | ||
} |
@@ -166,1 +166,21 @@ import { ElementDescriptor } from "@qawolf/types"; | ||
}; | ||
export const isClickable = ( | ||
element: HTMLElement, | ||
computedStyle: CSSStyleDeclaration | ||
) => { | ||
// assume it is clickable if the cursor is a pointer | ||
const clickable = computedStyle.cursor === "pointer"; | ||
return clickable && isVisible(element, computedStyle); | ||
}; | ||
export const isVisible = ( | ||
element: HTMLElement, | ||
computedStyle?: CSSStyleDeclaration | ||
): boolean => { | ||
if (element.offsetWidth <= 0 || element.offsetHeight <= 0) return false; | ||
if (computedStyle && computedStyle.visibility === "hidden") return false; | ||
return true; | ||
}; |
import { Action, Locator } from "@qawolf/types"; | ||
import { getDataValue, isClickable, isVisible } from "./element"; | ||
import { topMatch } from "./match"; | ||
import { waitFor } from "./wait"; | ||
import { findElementByXpath } from "./xpath"; | ||
import { findElementByXpath, getXpath } from "./xpath"; | ||
@@ -12,6 +13,2 @@ type QueryByDataArgs = { | ||
export const isVisible = (element: HTMLElement): boolean => { | ||
return !!(element.offsetWidth && element.offsetHeight); | ||
}; | ||
export const queryActionElements = (action: Action): HTMLElement[] => { | ||
@@ -44,2 +41,4 @@ const selector = | ||
for (let i = 0; i < elements.length; i++) { | ||
// we do not pass computedStyle because doing | ||
// that for every element would be very expensive | ||
if (isVisible(elements[i] as HTMLElement)) { | ||
@@ -53,2 +52,40 @@ visibleElements.push(elements[i] as HTMLElement); | ||
export const findClickableAncestor = ( | ||
element: HTMLElement, | ||
dataAttribute: string | ||
): HTMLElement => { | ||
/** | ||
* Crawl up until we reach the top "clickable" ancestor | ||
*/ | ||
let ancestor = element; | ||
console.log("find clickable ancestor for", getXpath(element)); | ||
while (ancestor.parentElement) { | ||
// short-circuit when we encounter a data value | ||
const dataValue = getDataValue(ancestor, dataAttribute); | ||
if (dataValue) { | ||
console.log( | ||
`found clickable ancestor with ${dataAttribute}="${dataValue}"`, | ||
getXpath(ancestor) | ||
); | ||
return ancestor; | ||
} | ||
// short-circuit when parent is not clickable | ||
if ( | ||
!isClickable( | ||
ancestor.parentElement, | ||
window.getComputedStyle(ancestor.parentElement) | ||
) | ||
) { | ||
console.log("found clickable ancestor", getXpath(ancestor)); | ||
return ancestor; | ||
} | ||
ancestor = ancestor.parentElement; | ||
} | ||
return ancestor; | ||
}; | ||
export const findElement = async ({ | ||
@@ -84,4 +121,5 @@ action, | ||
console.log("waiting for top strong match", target); | ||
const strongMatch = await waitFor(() => { | ||
console.log("waiting for top strong match", target); | ||
const elements = queryActionElements(action); | ||
@@ -88,0 +126,0 @@ return topMatch({ |
import * as types from "@qawolf/types"; | ||
import { getDescriptor } from "./element"; | ||
import { findClickableAncestor } from "./find"; | ||
@@ -56,9 +57,24 @@ type EventCallback = types.Callback<types.Event>; | ||
private recordEvents() { | ||
this.recordEvent("click", event => ({ | ||
isTrusted: event.isTrusted, | ||
name: "click", | ||
target: getDescriptor(event.target as HTMLElement, this._dataAttribute), | ||
time: Date.now() | ||
})); | ||
this.recordEvent("click", event => { | ||
// findClickableAncestor chooses the ancestor if it has a data-attribute | ||
// which is very likely the target we want to click on. | ||
// If there is not a data-attribute on any of the clickable ancestors | ||
// it will take the top most clickable ancestor. | ||
// The ancestor is likely a better target than the descendant. | ||
// Ex. when you click on the i (button > i) or rect (a > svg > rect) | ||
// chances are the ancestor (button, a) is a better target to find. | ||
// XXX if anyone runs into issues with this behavior we can allow disabling it from a flag. | ||
const target = findClickableAncestor( | ||
event.target as HTMLElement, | ||
this._dataAttribute | ||
); | ||
return { | ||
isTrusted: event.isTrusted, | ||
name: "click", | ||
target: getDescriptor(target, this._dataAttribute), | ||
time: Date.now() | ||
}; | ||
}); | ||
this.recordEvent("input", event => { | ||
@@ -65,0 +81,0 @@ const element = event.target as HTMLInputElement; |
@@ -23,3 +23,3 @@ export const sleep = (milliseconds: number): Promise<void> => { | ||
export const waitUntil = async ( | ||
booleanFn: () => boolean, | ||
booleanFn: () => boolean | Promise<boolean>, | ||
timeoutMs: number, | ||
@@ -31,3 +31,3 @@ sleepMs: number = 500 | ||
while (Date.now() - startTime < timeoutMs) { | ||
const conditionMet = booleanFn(); | ||
const conditionMet = await booleanFn(); | ||
if (conditionMet) return; | ||
@@ -34,0 +34,0 @@ |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
138852
76
2668