New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

@qawolf/web

Package Overview
Dependencies
Maintainers
2
Versions
40
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@qawolf/web - npm Package Compare versions

Comparing version 0.9.2 to 0.9.3-alpha.0

lib/buildCssSelector.d.ts

8

lib/element.d.ts

@@ -1,9 +0,3 @@

interface AttributeValuePair {
attribute: string;
value: string;
}
export declare const getClickableAncestor: (element: HTMLElement, attribute: string) => HTMLElement;
export declare const getAttributeValue: (element: HTMLElement, attribute: string) => AttributeValuePair | null;
export declare const getClickableAncestor: (element: HTMLElement) => HTMLElement;
export declare const isClickable: (element: HTMLElement, computedStyle: CSSStyleDeclaration) => boolean;
export declare const isVisible: (element: Element, computedStyle?: CSSStyleDeclaration | undefined) => boolean;
export {};
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const xpath_1 = require("./xpath");
exports.getClickableAncestor = (element, attribute) => {
exports.getClickableAncestor = (element) => {
let ancestor = element;
console.debug("qawolf: get clickable ancestor for", xpath_1.getXpath(element));
while (ancestor.parentElement) {
const attributeValue = exports.getAttributeValue(ancestor, attribute);
if (attributeValue) {
console.debug(`qawolf: found clickable ancestor: ${JSON.stringify(attributeValue)}"`, xpath_1.getXpath(ancestor));
return ancestor;
}
if (["a", "button", "input"].includes(ancestor.tagName.toLowerCase())) {

@@ -18,20 +13,10 @@ console.debug(`qawolf: found clickable ancestor: ${ancestor.tagName}`, xpath_1.getXpath(ancestor));

if (!exports.isClickable(ancestor.parentElement, window.getComputedStyle(ancestor.parentElement))) {
console.debug("qawolf: no clickable ancestor, use target", xpath_1.getXpath(element));
return element;
console.debug("qawolf: found clickable ancestor", xpath_1.getXpath(ancestor));
return ancestor;
}
ancestor = ancestor.parentElement;
}
console.debug("qawolf: found clickable ancestor", xpath_1.getXpath(ancestor));
return ancestor;
};
exports.getAttributeValue = (element, attribute) => {
if (!attribute)
return null;
const attributes = attribute.split(",").map(attr => attr.trim());
for (let attribute of attributes) {
const value = element.getAttribute(attribute);
if (value)
return { attribute, value };
}
return null;
};
exports.isClickable = (element, computedStyle) => {

@@ -38,0 +23,0 @@ const clickable = computedStyle.cursor === "pointer";

import { ElementEvent } from "@qawolf/types";
export declare const isKeyEvent: (event: ElementEvent | null) => boolean | null;
export declare const isPasteEvent: (event: ElementEvent | null) => boolean | null;
export declare const isSelectAllEvent: (event: ElementEvent | null) => boolean | null;
export declare const isTypeEvent: (event: ElementEvent | null) => boolean | null;

@@ -7,3 +7,4 @@ "use strict";

exports.isPasteEvent = (event) => event && event.isTrusted && event.name === "paste";
exports.isSelectAllEvent = (event) => event && event.isTrusted && event.name === "selectall";
exports.isTypeEvent = (event) => exports.isKeyEvent(event) || exports.isPasteEvent(event);
//# sourceMappingURL=event.js.map

@@ -18,4 +18,6 @@ "use strict";

}
description = description.replace("'", "");
description = description.replace(/[^\x20-\x7E]/g, "");
if (description.length) {
return ` "${description}"`.replace("'", "");
return ` "${description}"`;
}

@@ -22,0 +24,0 @@ return "";

@@ -12,3 +12,3 @@ import * as element from "./element";

declare const describeDoc: (html: import("@qawolf/types").DocSelector) => string;
declare const isKeyEvent: (event: import("@qawolf/types").ElementEvent | null) => boolean | null, isPasteEvent: (event: import("@qawolf/types").ElementEvent | null) => boolean | null, isTypeEvent: (event: import("@qawolf/types").ElementEvent | null) => boolean | null;
declare const isKeyEvent: (event: import("@qawolf/types").ElementEvent | null) => boolean | null, isPasteEvent: (event: import("@qawolf/types").ElementEvent | null) => boolean | null, isSelectAllEvent: (event: import("@qawolf/types").ElementEvent | null) => boolean | null, isTypeEvent: (event: import("@qawolf/types").ElementEvent | null) => boolean | null;
declare const compareAttributes: (a: any, b: any) => {

@@ -24,4 +24,5 @@ attrs: {

declare const sleep: (milliseconds: number) => Promise<void>, waitFor: <T>(valueFunction: () => T | Promise<T> | null, timeoutMs: number, sleepMs?: number) => Promise<T | null>, waitUntil: (predicate: () => boolean | Promise<boolean>, timeoutMs: number, sleepMs?: number) => Promise<void>;
export { compareAttributes, compareContent, compareDoc, describeDoc, decodeHtml, htmlToDoc, isKeyEvent, isNil, isPasteEvent, isTypeEvent, matchDocSelector, serializeDocSelector, sleep, waitFor, waitUntil };
export { compareAttributes, compareContent, compareDoc, describeDoc, decodeHtml, htmlToDoc, isKeyEvent, isNil, isPasteEvent, isSelectAllEvent, isTypeEvent, matchDocSelector, serializeDocSelector, sleep, waitFor, waitUntil };
declare const webExports: {
buildCssSelector: ({ attribute, isClick, target }: import("./buildCssSelector").BuildCssSelectorOptions) => string | undefined;
captureLogs: (logLevel: string, callback: (level: string, message: string) => void) => void;

@@ -32,2 +33,3 @@ element: typeof element;

format: typeof format;
getAttributeValue: (element: HTMLElement, attribute: string) => import("./buildCssSelector").AttributeValuePair | null;
lang: typeof lang;

@@ -34,0 +36,0 @@ Recorder: typeof Recorder;

@@ -10,2 +10,3 @@ "use strict";

Object.defineProperty(exports, "__esModule", { value: true });
const buildCssSelector_1 = require("./buildCssSelector");
const captureLogs_1 = require("./captureLogs");

@@ -25,5 +26,6 @@ const element = __importStar(require("./element"));

exports.describeDoc = describeDoc;
const { isKeyEvent, isPasteEvent, isTypeEvent } = event;
const { isKeyEvent, isPasteEvent, isSelectAllEvent, isTypeEvent } = event;
exports.isKeyEvent = isKeyEvent;
exports.isPasteEvent = isPasteEvent;
exports.isSelectAllEvent = isSelectAllEvent;
exports.isTypeEvent = isTypeEvent;

@@ -46,2 +48,3 @@ const { compareAttributes, compareContent, compareDoc, matchDocSelector } = find;

const webExports = {
buildCssSelector: buildCssSelector_1.buildCssSelector,
captureLogs: captureLogs_1.captureLogs,

@@ -52,2 +55,3 @@ element,

format,
getAttributeValue: buildCssSelector_1.getAttributeValue,
lang,

@@ -54,0 +58,0 @@ Recorder: Recorder_1.Recorder,

@@ -56,2 +56,75 @@ var qawolf = (function (exports) {

var buildCssSelector = function (_a) {
var attribute = _a.attribute, isClick = _a.isClick, target = _a.target;
// find the closest element to the target with attribute
var elementWithSelector = findAttribute(target, attribute);
if (!elementWithSelector) {
console.debug("qawolf: no css selector built. attribute not found on target or ancestors " + attribute, getXpath(target));
return undefined;
}
var attributeValue = elementWithSelector.attributeValue;
var attributeSelector = "[" + attributeValue.attribute + "='" + attributeValue.value + "']";
if (elementWithSelector.element === target) {
console.debug("qawolf: css selector built for target " + attributeSelector, getXpath(target));
return attributeSelector;
}
var descendantSelector = buildDescendantSelector(target, elementWithSelector.element, isClick);
var targetSelector = "" + attributeSelector + descendantSelector;
console.debug("qawolf: css selector built for ancestor " + targetSelector, getXpath(target));
return targetSelector;
};
var buildDescendantSelector = function (descendant, ancestor, isClick) {
var inputElement = descendant;
if (["checkbox", "radio"].includes(inputElement.type) && inputElement.value) {
// Target the value for these input types
return " [value='" + inputElement.value + "']";
}
if (isClick) {
return findClickableDescendantTag(descendant, ancestor);
}
if (descendant.contentEditable === "true") {
return " [contenteditable='true']";
}
return " " + descendant.tagName.toLowerCase();
};
var findAttribute = function (element, attribute) {
var ancestor = element;
while (ancestor) {
var attributeValue = getAttributeValue(ancestor, attribute);
if (attributeValue) {
return { attributeValue: attributeValue, element: ancestor };
}
ancestor = ancestor.parentElement;
}
return null;
};
var findClickableDescendantTag = function (descendant, ancestor) {
/**
* Target common clickable descendant tags.
* Ex. the DatePicker's date button
*/
var parent = descendant;
// stop when we hit the ancestor
while (parent !== ancestor) {
var tagName = parent.tagName.toLowerCase();
if (["a", "button", "input"].includes(tagName)) {
return " " + tagName;
}
parent = parent.parentElement;
}
return "";
};
var getAttributeValue = function (element, attribute) {
if (!attribute || !element.getAttribute)
return null;
var attributes = attribute.split(",").map(function (attr) { return attr.trim(); });
for (var _i = 0, attributes_1 = attributes; _i < attributes_1.length; _i++) {
var attribute_1 = attributes_1[_i];
var value = element.getAttribute(attribute_1);
if (value)
return { attribute: attribute_1, value: value };
}
return null;
};
var LOG_LEVELS = ["debug", "error", "info", "log", "warn"];

@@ -98,7 +171,6 @@ var formatArgument = function (argument) {

var getClickableAncestor = function (element, attribute) {
var getClickableAncestor = function (element) {
/**
* Crawl up until we reach the top "clickable" ancestor.
* If a target is the descendant of a clickable element with the attribute choose it.
* If the target is the descendant of "a"/"button"/"input" choose it.
* If the target is the descendant of "a"/"button"/"input" or a clickable element choose it.
* Otherwise choose the original element as the target.

@@ -109,34 +181,18 @@ */

while (ancestor.parentElement) {
// choose the data value element as the clickable ancestor
var attributeValue = getAttributeValue(ancestor, attribute);
if (attributeValue) {
console.debug("qawolf: found clickable ancestor: " + JSON.stringify(attributeValue) + "\"", getXpath(ancestor));
return ancestor;
}
// choose the common clickable element type as the clickable ancestor
if (["a", "button", "input"].includes(ancestor.tagName.toLowerCase())) {
// stop crawling when the ancestor is a good clickable tag
console.debug("qawolf: found clickable ancestor: " + ancestor.tagName, getXpath(ancestor));
return ancestor;
}
// stop crawling at the first non-clickable element
if (!isClickable(ancestor.parentElement, window.getComputedStyle(ancestor.parentElement))) {
console.debug("qawolf: no clickable ancestor, use target", getXpath(element));
return element;
// stop crawling at the first non-clickable element
console.debug("qawolf: found clickable ancestor", getXpath(ancestor));
return ancestor;
}
ancestor = ancestor.parentElement;
}
// stop crawling at the root
console.debug("qawolf: found clickable ancestor", getXpath(ancestor));
return ancestor;
};
var getAttributeValue = function (element, attribute) {
if (!attribute)
return null;
var attributes = attribute.split(",").map(function (attr) { return attr.trim(); });
for (var _i = 0, attributes_1 = attributes; _i < attributes_1.length; _i++) {
var attribute_1 = attributes_1[_i];
var value = element.getAttribute(attribute_1);
if (value)
return { attribute: attribute_1, value: value };
}
return null;
};
var isClickable = function (element, computedStyle) {

@@ -161,3 +217,2 @@ // assume it is clickable if the cursor is a pointer

getClickableAncestor: getClickableAncestor,
getAttributeValue: getAttributeValue,
isClickable: isClickable,

@@ -175,2 +230,5 @@ isVisible: isVisible

};
var isSelectAllEvent = function (event) {
return event && event.isTrusted && event.name === "selectall";
};
var isTypeEvent = function (event) {

@@ -184,2 +242,3 @@ return isKeyEvent(event) || isPasteEvent(event);

isPasteEvent: isPasteEvent,
isSelectAllEvent: isSelectAllEvent,
isTypeEvent: isTypeEvent

@@ -333,2 +392,13 @@ });

var __assign = function() {
__assign = Object.assign || function __assign(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
function __awaiter(thisArg, _arguments, P, generator) {

@@ -463,5 +533,10 @@ return new (P || (P = Promise))(function (resolve, reject) {

}
// strip single quotes so the description is valid javascript
description = description.replace("'", "");
// remove invisible characters which look like an empty string but have a length
// https://www.w3resource.com/javascript-exercises/javascript-string-exercise-32.php
// https://stackoverflow.com/a/21797208/230462
description = description.replace(/[^\x20-\x7E]/g, "");
if (description.length) {
// strip single quotes so the description is valid javascript
return (" \"" + description + "\"").replace("'", "");
return " \"" + description + "\"";
}

@@ -981,19 +1056,28 @@ return "";

};
Recorder.prototype.recordEvent = function (eventName, handler) {
var _this = this;
this.listen(eventName, function (ev) {
var event = handler(ev);
if (!event)
return;
console.debug("qawolf: Recorder " + _this._id + ": " + eventName + " event", ev, ev.target, "recorded:", event);
_this._sendEvent(event);
});
Recorder.prototype.sendEvent = function (eventName, event, value) {
var target = event.target;
var elementEvent = {
cssSelector: buildCssSelector({
attribute: this._attribute,
isClick: eventName === "click" || eventName === "mousedown",
target: target
}),
isTrusted: event.isTrusted,
name: eventName,
page: this._pageIndex,
target: nodeToDocSelector(target),
time: Date.now(),
value: value
};
console.debug("qawolf: Recorder " + this._id + ": " + eventName + " event", event, event.target, "recorded:", elementEvent);
this._sendEvent(elementEvent);
};
Recorder.prototype.recordEvents = function () {
var _this = this;
this.recordEvent("click", function (event) {
// getClickableAncestor 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.
this.listen("mousedown", function (event) {
// only the main button (not right clicks/etc)
// https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button
if (event.button !== 0)
return;
// getClickableAncestor chooses the top most clickable ancestor.
// The ancestor is likely a better target than the descendant.

@@ -1003,53 +1087,42 @@ // Ex. when you click on the i (button > i) or rect (a > svg > rect)

// XXX if anyone runs into issues with this behavior we can allow disabling it from a flag.
var target = getClickableAncestor(event.target, _this._attribute);
return {
isTrusted: event.isTrusted,
name: "click",
page: _this._pageIndex,
target: nodeToDocSelector(target),
time: Date.now()
};
var target = getClickableAncestor(event.target);
_this.sendEvent("mousedown", __assign(__assign({}, event), { target: target }));
});
this.recordEvent("input", function (event) {
var element = event.target;
this.listen("click", function (event) {
if (event.button !== 0)
return;
var target = getClickableAncestor(event.target);
_this.sendEvent("click", __assign(__assign({}, event), { target: target }));
});
this.listen("input", function (event) {
var target = event.target;
// ignore input events not on selects
if (element.tagName.toLowerCase() !== "select")
// other input events are captured in click and type listeners
if (target.tagName.toLowerCase() !== "select")
return;
return {
isTrusted: event.isTrusted,
name: "input",
page: _this._pageIndex,
target: nodeToDocSelector(element),
time: Date.now(),
value: element.value
};
_this.sendEvent("input", event, target.value);
});
this.recordEvent("keydown", function (event) { return ({
isTrusted: event.isTrusted,
name: "keydown",
page: _this._pageIndex,
target: nodeToDocSelector(event.target),
time: Date.now(),
value: event.key
}); });
this.recordEvent("keyup", function (event) { return ({
isTrusted: event.isTrusted,
name: "keyup",
page: _this._pageIndex,
target: nodeToDocSelector(event.target),
time: Date.now(),
value: event.key
}); });
this.recordEvent("paste", function (event) {
this.listen("keydown", function (event) {
_this.sendEvent("keydown", event, event.key);
});
this.listen("keyup", function (event) {
_this.sendEvent("keyup", event, event.key);
});
this.listen("paste", function (event) {
if (!event.clipboardData)
return;
return {
isTrusted: event.isTrusted,
name: "paste",
page: _this._pageIndex,
target: nodeToDocSelector(event.target),
time: Date.now(),
value: event.clipboardData.getData("text")
};
var value = event.clipboardData.getData("text");
_this.sendEvent("paste", event, value);
});
// XXX select only supports input/textarea
// We can combine selectstart/mouseup to support content editables
this.listen("select", function (event) {
var target = event.target;
if (target.selectionStart !== 0 ||
target.selectionEnd !== target.value.length) {
// Only record select all, not other selection events
return;
}
_this.sendEvent("selectall", event);
});
this.recordScrollEvent();

@@ -1063,3 +1136,3 @@ };

// because it fires after the element.scrollLeft & element.scrollTop are updated
this.recordEvent("scroll", function (event) {
this.listen("scroll", function (event) {
if (!lastWheelEvent || event.timeStamp - lastWheelEvent.timeStamp > 100) {

@@ -1073,18 +1146,12 @@ // We record mouse wheel initiated scrolls only

}
var element = event.target;
var target = event.target;
if (event.target === document || event.target === document.body) {
element = (document.scrollingElement ||
target = (document.scrollingElement ||
document.documentElement);
}
return {
isTrusted: event.isTrusted,
name: "scroll",
page: _this._pageIndex,
target: nodeToDocSelector(element),
time: Date.now(),
value: {
x: element.scrollLeft,
y: element.scrollTop
}
var value = {
x: target.scrollLeft,
y: target.scrollTop
};
_this.sendEvent("scroll", __assign(__assign({}, event), { target: target }), value);
});

@@ -1183,3 +1250,3 @@ };

var describeDoc$1 = describeDoc;
var isKeyEvent$1 = isKeyEvent, isPasteEvent$1 = isPasteEvent, isTypeEvent$1 = isTypeEvent;
var isKeyEvent$1 = isKeyEvent, isPasteEvent$1 = isPasteEvent, isSelectAllEvent$1 = isSelectAllEvent, isTypeEvent$1 = isTypeEvent;
var compareAttributes$1 = compareAttributes, compareContent$1 = compareContent, compareDoc$1 = compareDoc, matchDocSelector$1 = matchDocSelector;

@@ -1191,2 +1258,3 @@ var decodeHtml$1 = decodeHtml, isNil$1 = isNil;

var webExports = {
buildCssSelector: buildCssSelector,
captureLogs: captureLogs,

@@ -1197,2 +1265,3 @@ element: element,

format: format,
getAttributeValue: getAttributeValue,
lang: lang,

@@ -1219,2 +1288,3 @@ Recorder: Recorder,

exports.isPasteEvent = isPasteEvent$1;
exports.isSelectAllEvent = isSelectAllEvent$1;
exports.isTypeEvent = isTypeEvent$1;

@@ -1221,0 +1291,0 @@ exports.matchDocSelector = matchDocSelector$1;

@@ -12,3 +12,3 @@ import * as types from "@qawolf/types";

private listen;
private recordEvent;
private sendEvent;
private recordEvents;

@@ -15,0 +15,0 @@ private recordScrollEvent;

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const buildCssSelector_1 = require("./buildCssSelector");
const element_1 = require("./element");

@@ -26,63 +27,59 @@ const serialize_1 = require("./serialize");

}
recordEvent(eventName, handler) {
this.listen(eventName, (ev) => {
const event = handler(ev);
if (!event)
return;
console.debug(`qawolf: Recorder ${this._id}: ${eventName} event`, ev, ev.target, "recorded:", event);
this._sendEvent(event);
});
sendEvent(eventName, event, value) {
const target = event.target;
const elementEvent = {
cssSelector: buildCssSelector_1.buildCssSelector({
attribute: this._attribute,
isClick: eventName === "click" || eventName === "mousedown",
target
}),
isTrusted: event.isTrusted,
name: eventName,
page: this._pageIndex,
target: serialize_1.nodeToDocSelector(target),
time: Date.now(),
value
};
console.debug(`qawolf: Recorder ${this._id}: ${eventName} event`, event, event.target, "recorded:", elementEvent);
this._sendEvent(elementEvent);
}
recordEvents() {
this.recordEvent("click", event => {
const target = element_1.getClickableAncestor(event.target, this._attribute);
return {
isTrusted: event.isTrusted,
name: "click",
page: this._pageIndex,
target: serialize_1.nodeToDocSelector(target),
time: Date.now()
};
this.listen("mousedown", event => {
if (event.button !== 0)
return;
const target = element_1.getClickableAncestor(event.target);
this.sendEvent("mousedown", Object.assign(Object.assign({}, event), { target }));
});
this.recordEvent("input", event => {
const element = event.target;
if (element.tagName.toLowerCase() !== "select")
this.listen("click", event => {
if (event.button !== 0)
return;
return {
isTrusted: event.isTrusted,
name: "input",
page: this._pageIndex,
target: serialize_1.nodeToDocSelector(element),
time: Date.now(),
value: element.value
};
const target = element_1.getClickableAncestor(event.target);
this.sendEvent("click", Object.assign(Object.assign({}, event), { target }));
});
this.recordEvent("keydown", event => ({
isTrusted: event.isTrusted,
name: "keydown",
page: this._pageIndex,
target: serialize_1.nodeToDocSelector(event.target),
time: Date.now(),
value: event.key
}));
this.recordEvent("keyup", event => ({
isTrusted: event.isTrusted,
name: "keyup",
page: this._pageIndex,
target: serialize_1.nodeToDocSelector(event.target),
time: Date.now(),
value: event.key
}));
this.recordEvent("paste", event => {
this.listen("input", event => {
const target = event.target;
if (target.tagName.toLowerCase() !== "select")
return;
this.sendEvent("input", event, target.value);
});
this.listen("keydown", event => {
this.sendEvent("keydown", event, event.key);
});
this.listen("keyup", event => {
this.sendEvent("keyup", event, event.key);
});
this.listen("paste", event => {
if (!event.clipboardData)
return;
return {
isTrusted: event.isTrusted,
name: "paste",
page: this._pageIndex,
target: serialize_1.nodeToDocSelector(event.target),
time: Date.now(),
value: event.clipboardData.getData("text")
};
const value = event.clipboardData.getData("text");
this.sendEvent("paste", event, value);
});
this.listen("select", event => {
const target = event.target;
if (target.selectionStart !== 0 ||
target.selectionEnd !== target.value.length) {
return;
}
this.sendEvent("selectall", event);
});
this.recordScrollEvent();

@@ -93,3 +90,3 @@ }

this.listen("wheel", ev => (lastWheelEvent = ev));
this.recordEvent("scroll", event => {
this.listen("scroll", event => {
if (!lastWheelEvent || event.timeStamp - lastWheelEvent.timeStamp > 100) {

@@ -99,18 +96,12 @@ console.debug("qawolf: ignore non-wheel scroll event", event);

}
let element = event.target;
let target = event.target;
if (event.target === document || event.target === document.body) {
element = (document.scrollingElement ||
target = (document.scrollingElement ||
document.documentElement);
}
return {
isTrusted: event.isTrusted,
name: "scroll",
page: this._pageIndex,
target: serialize_1.nodeToDocSelector(element),
time: Date.now(),
value: {
x: element.scrollLeft,
y: element.scrollTop
}
const value = {
x: target.scrollLeft,
y: target.scrollTop
};
this.sendEvent("scroll", Object.assign(Object.assign({}, event), { target }), value);
});

@@ -117,0 +108,0 @@ }

{
"name": "@qawolf/web",
"description": "qawolf web library",
"version": "0.9.2",
"version": "0.9.3-alpha.0",
"license": "BSD-3.0",

@@ -30,3 +30,3 @@ "main": "./lib/index.js",

"devDependencies": {
"@qawolf/types": "0.9.2",
"@qawolf/types": "0.9.3-alpha.0",
"rollup": "^1.23.1",

@@ -37,3 +37,3 @@ "rollup-plugin-commonjs": "^10.1.0",

},
"gitHead": "c9e483b27df4203243b4604e8268c0885c612e7e"
"gitHead": "4a729506a9cbe8fe126682d77000727472a440ba"
}
import { getXpath } from "./xpath";
interface AttributeValuePair {
attribute: string;
value: string;
}
export const getClickableAncestor = (
element: HTMLElement,
attribute: string
): HTMLElement => {
export const getClickableAncestor = (element: HTMLElement): HTMLElement => {
/**
* Crawl up until we reach the top "clickable" ancestor.
* If a target is the descendant of a clickable element with the attribute choose it.
* If the target is the descendant of "a"/"button"/"input" choose it.
* If the target is the descendant of "a"/"button"/"input" or a clickable element choose it.
* Otherwise choose the original element as the target.

@@ -22,15 +13,4 @@ */

while (ancestor.parentElement) {
// choose the data value element as the clickable ancestor
const attributeValue = getAttributeValue(ancestor, attribute);
if (attributeValue) {
console.debug(
`qawolf: found clickable ancestor: ${JSON.stringify(attributeValue)}"`,
getXpath(ancestor)
);
return ancestor;
}
// choose the common clickable element type as the clickable ancestor
if (["a", "button", "input"].includes(ancestor.tagName.toLowerCase())) {
// stop crawling when the ancestor is a good clickable tag
console.debug(

@@ -43,3 +23,2 @@ `qawolf: found clickable ancestor: ${ancestor.tagName}`,

// stop crawling at the first non-clickable element
if (

@@ -51,7 +30,5 @@ !isClickable(

) {
console.debug(
"qawolf: no clickable ancestor, use target",
getXpath(element)
);
return element;
// stop crawling at the first non-clickable element
console.debug("qawolf: found clickable ancestor", getXpath(ancestor));
return ancestor;
}

@@ -62,20 +39,8 @@

// stop crawling at the root
console.debug("qawolf: found clickable ancestor", getXpath(ancestor));
return ancestor;
};
export const getAttributeValue = (
element: HTMLElement,
attribute: string
): AttributeValuePair | null => {
if (!attribute) return null;
const attributes = attribute.split(",").map(attr => attr.trim());
for (let attribute of attributes) {
const value = element.getAttribute(attribute);
if (value) return { attribute, value };
}
return null;
};
export const isClickable = (

@@ -82,0 +47,0 @@ element: HTMLElement,

@@ -11,3 +11,6 @@ import { ElementEvent } from "@qawolf/types";

export const isSelectAllEvent = (event: ElementEvent | null) =>
event && event.isTrusted && event.name === "selectall";
export const isTypeEvent = (event: ElementEvent | null) =>
isKeyEvent(event) || isPasteEvent(event);

@@ -10,3 +10,3 @@ import { DocSelector } from "@qawolf/types";

// ex. "departure date"
let description =
let description: string =
attrs.labels ||

@@ -26,5 +26,12 @@ attrs.name ||

// strip single quotes so the description is valid javascript
description = description.replace("'", "");
// remove invisible characters which look like an empty string but have a length
// https://www.w3resource.com/javascript-exercises/javascript-string-exercise-32.php
// https://stackoverflow.com/a/21797208/230462
description = description.replace(/[^\x20-\x7E]/g, "");
if (description.length) {
// strip single quotes so the description is valid javascript
return ` "${description}"`.replace("'", "");
return ` "${description}"`;
}

@@ -31,0 +38,0 @@

@@ -0,1 +1,2 @@

import { buildCssSelector, getAttributeValue } from "./buildCssSelector";
import { captureLogs } from "./captureLogs";

@@ -16,3 +17,3 @@ import * as element from "./element";

const { describeDoc } = format;
const { isKeyEvent, isPasteEvent, isTypeEvent } = event;
const { isKeyEvent, isPasteEvent, isSelectAllEvent, isTypeEvent } = event;
const {

@@ -37,2 +38,3 @@ compareAttributes,

isPasteEvent,
isSelectAllEvent,
isTypeEvent,

@@ -48,2 +50,3 @@ matchDocSelector,

const webExports = {
buildCssSelector,
captureLogs,

@@ -54,2 +57,3 @@ element,

format,
getAttributeValue,
lang,

@@ -56,0 +60,0 @@ Recorder,

import * as types from "@qawolf/types";
import { buildCssSelector } from "./buildCssSelector";
import { getClickableAncestor } from "./element";

@@ -43,27 +44,40 @@ import { nodeToDocSelector } from "./serialize";

private recordEvent<K extends keyof DocumentEventMap>(
eventName: K,
handler: (ev: DocumentEventMap[K]) => types.ElementEvent | undefined
private sendEvent<K extends keyof DocumentEventMap>(
eventName: types.ElementEventName,
event: DocumentEventMap[K],
value?: string | types.ScrollValue | null
) {
this.listen(eventName, (ev: DocumentEventMap[K]) => {
const event = handler(ev);
if (!event) return;
const target = event.target as HTMLElement;
console.debug(
`qawolf: Recorder ${this._id}: ${eventName} event`,
ev,
ev.target,
"recorded:",
event
);
this._sendEvent(event);
});
const elementEvent = {
cssSelector: buildCssSelector({
attribute: this._attribute,
isClick: eventName === "click" || eventName === "mousedown",
target
}),
isTrusted: event.isTrusted,
name: eventName,
page: this._pageIndex,
target: nodeToDocSelector(target),
time: Date.now(),
value
};
console.debug(
`qawolf: Recorder ${this._id}: ${eventName} event`,
event,
event.target,
"recorded:",
elementEvent
);
this._sendEvent(elementEvent);
}
private recordEvents() {
this.recordEvent("click", event => {
// getClickableAncestor 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.
this.listen("mousedown", event => {
// only the main button (not right clicks/etc)
// https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button
if (event.button !== 0) return;
// getClickableAncestor chooses the top most clickable ancestor.
// The ancestor is likely a better target than the descendant.

@@ -73,63 +87,53 @@ // Ex. when you click on the i (button > i) or rect (a > svg > rect)

// XXX if anyone runs into issues with this behavior we can allow disabling it from a flag.
const target = getClickableAncestor(
event.target as HTMLElement,
this._attribute
);
const target = getClickableAncestor(event.target as HTMLElement);
this.sendEvent("mousedown", { ...event, target });
});
return {
isTrusted: event.isTrusted,
name: "click",
page: this._pageIndex,
target: nodeToDocSelector(target),
time: Date.now()
};
this.listen("click", event => {
if (event.button !== 0) return;
const target = getClickableAncestor(event.target as HTMLElement);
this.sendEvent("click", { ...event, target });
});
this.recordEvent("input", event => {
const element = event.target as HTMLInputElement;
this.listen("input", event => {
const target = event.target as HTMLInputElement;
// ignore input events not on selects
if (element.tagName.toLowerCase() !== "select") return;
// other input events are captured in click and type listeners
if (target.tagName.toLowerCase() !== "select") return;
return {
isTrusted: event.isTrusted,
name: "input",
page: this._pageIndex,
target: nodeToDocSelector(element),
time: Date.now(),
value: element.value
};
this.sendEvent("input", event, target.value);
});
this.recordEvent("keydown", event => ({
isTrusted: event.isTrusted,
name: "keydown",
page: this._pageIndex,
target: nodeToDocSelector(event.target as HTMLElement),
time: Date.now(),
value: event.key
}));
this.listen("keydown", event => {
this.sendEvent("keydown", event, event.key);
});
this.recordEvent("keyup", event => ({
isTrusted: event.isTrusted,
name: "keyup",
page: this._pageIndex,
target: nodeToDocSelector(event.target as HTMLElement),
time: Date.now(),
value: event.key
}));
this.listen("keyup", event => {
this.sendEvent("keyup", event, event.key);
});
this.recordEvent("paste", event => {
this.listen("paste", event => {
if (!event.clipboardData) return;
return {
isTrusted: event.isTrusted,
name: "paste",
page: this._pageIndex,
target: nodeToDocSelector(event.target as HTMLElement),
time: Date.now(),
value: event.clipboardData.getData("text")
};
const value = event.clipboardData.getData("text");
this.sendEvent("paste", event, value);
});
// XXX select only supports input/textarea
// We can combine selectstart/mouseup to support content editables
this.listen("select", event => {
const target = event.target as HTMLInputElement;
if (
target.selectionStart !== 0 ||
target.selectionEnd !== target.value.length
) {
// Only record select all, not other selection events
return;
}
this.sendEvent("selectall", event);
});
this.recordScrollEvent();

@@ -144,3 +148,3 @@ }

// because it fires after the element.scrollLeft & element.scrollTop are updated
this.recordEvent("scroll", event => {
this.listen("scroll", event => {
if (!lastWheelEvent || event.timeStamp - lastWheelEvent.timeStamp > 100) {

@@ -155,21 +159,16 @@ // We record mouse wheel initiated scrolls only

let element = event.target as HTMLElement;
let target = event.target as HTMLElement;
if (event.target === document || event.target === document.body) {
element = (document.scrollingElement ||
target = (document.scrollingElement ||
document.documentElement) as HTMLElement;
}
return {
isTrusted: event.isTrusted,
name: "scroll",
page: this._pageIndex,
target: nodeToDocSelector(element),
time: Date.now(),
value: {
x: element.scrollLeft,
y: element.scrollTop
}
const value = {
x: target.scrollLeft,
y: target.scrollTop
};
this.sendEvent("scroll", { ...event, target }, value);
});
}
}

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

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc