From 7d3bd22cd41e63149a2c4d8b6b7e198ec7df3c33 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Wed, 5 Jun 2024 06:26:13 +0530 Subject: [PATCH 01/83] feat: find the HTML element at the specified coordinates + cast it to an HTMLElement type --- server/src/workflow-management/selector.ts | 34 ++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 server/src/workflow-management/selector.ts diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts new file mode 100644 index 00000000..d16f5cd1 --- /dev/null +++ b/server/src/workflow-management/selector.ts @@ -0,0 +1,34 @@ +import { Page } from "playwright"; +import { Action, ActionType, Coordinates, TagName } from "../types"; +import { WhereWhatPair, WorkflowFile } from "@wbr-project/wbr-interpret"; +import logger from "../logger"; +import { getBestSelectorForAction } from "./utils"; + +/** + * Returns a {@link Rectangle} object representing + * the coordinates, width, height and corner points of the element. + * If an element is not found, returns null. + * @param page The page instance. + * @param coordinates Coordinates of an element. + * @category WorkflowManagement-Selectors + * @returns {Promise} + */ +export const getRect = async (page: Page, coordinates: Coordinates) => { + try { + const rect = await page.evaluate( + async ({ x, y }) => { + const el = document.elementFromPoint(x, y) as HTMLElement; + ); + + } catch (error) { + const { message, stack } = error as Error; + logger.log('error', `Error while retrieving selector: ${message}`); + logger.log('error', `Stack: ${stack}`); + } +} + + + + + + From 074cc697b29fb70f2658f40552e2cb9324a09505 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Wed, 5 Jun 2024 06:27:17 +0530 Subject: [PATCH 02/83] feat: check if element found @ coordinates --- server/src/workflow-management/selector.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index d16f5cd1..516b9bf0 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -18,6 +18,11 @@ export const getRect = async (page: Page, coordinates: Coordinates) => { const rect = await page.evaluate( async ({ x, y }) => { const el = document.elementFromPoint(x, y) as HTMLElement; + if (el) { + const { parentElement } = el; + + }}, + ); } catch (error) { From 59f778a0c64c87f5d52e464fa4eafccf0f86c938 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Wed, 5 Jun 2024 06:28:49 +0530 Subject: [PATCH 03/83] feat: check if parent el's tag name is a + use parent elt if true, otherwise uses the original element --- server/src/workflow-management/selector.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index 516b9bf0..167e9804 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -20,6 +20,8 @@ export const getRect = async (page: Page, coordinates: Coordinates) => { const el = document.elementFromPoint(x, y) as HTMLElement; if (el) { const { parentElement } = el; + // Match the logic in recorder.ts for link clicks + const element = parentElement?.tagName === 'A' ? parentElement : el; }}, From cf85b238f086a68acdc414d506b1591f3197dca1 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Wed, 5 Jun 2024 06:31:58 +0530 Subject: [PATCH 04/83] feat: retrieve bounding rectangle of element & store in rectangle --- server/src/workflow-management/selector.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index 167e9804..879a4329 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -22,6 +22,7 @@ export const getRect = async (page: Page, coordinates: Coordinates) => { const { parentElement } = el; // Match the logic in recorder.ts for link clicks const element = parentElement?.tagName === 'A' ? parentElement : el; + const rectangle = element?.getBoundingClientRect(); }}, From 6774d74ccc3e4d2503f880f49f0ca28ceb47eada Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Wed, 5 Jun 2024 06:32:45 +0530 Subject: [PATCH 05/83] feat: if valid rect, return rect properties --- server/src/workflow-management/selector.ts | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index 879a4329..5a4fe3bd 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -23,11 +23,23 @@ export const getRect = async (page: Page, coordinates: Coordinates) => { // Match the logic in recorder.ts for link clicks const element = parentElement?.tagName === 'A' ? parentElement : el; const rectangle = element?.getBoundingClientRect(); - + // @ts-ignore + if (rectangle) { + return { + x: rectangle.x, + y: rectangle.y, + width: rectangle.width, + height: rectangle.height, + top: rectangle.top, + right: rectangle.right, + bottom: rectangle.bottom, + left: rectangle.left, + }; + } }}, - + { x: coordinates.x, y: coordinates.y }, ); - + return rect; } catch (error) { const { message, stack } = error as Error; logger.log('error', `Error while retrieving selector: ${message}`); From 39ca2a2618b8ae9f30741516e52a863ca21ccada Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Wed, 5 Jun 2024 06:34:46 +0530 Subject: [PATCH 06/83] feat: add Workflow type --- server/src/workflow-management/selector.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index 5a4fe3bd..8526e6ec 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -4,6 +4,8 @@ import { WhereWhatPair, WorkflowFile } from "@wbr-project/wbr-interpret"; import logger from "../logger"; import { getBestSelectorForAction } from "./utils"; +type Workflow = WorkflowFile["workflow"]; + /** * Returns a {@link Rectangle} object representing * the coordinates, width, height and corner points of the element. From 1b176f8e1695234e02bb8c169e2ebe49c436dd6c Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Wed, 5 Jun 2024 07:05:20 +0530 Subject: [PATCH 07/83] feat: get element information --- server/src/workflow-management/selector.ts | 30 ++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index 8526e6ec..32363da2 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -50,6 +50,36 @@ export const getRect = async (page: Page, coordinates: Coordinates) => { } +export const getElementInformation = async ( + page: Page, + coordinates: Coordinates +) => { + try { + const elementInfo = await page.evaluate( + async ({ x, y }) => { + const el = document.elementFromPoint(x, y) as HTMLElement; + if ( el ) { + const { parentElement } = el; + // Match the logic in recorder.ts for link clicks + const element = parentElement?.tagName === 'A' ? parentElement : el; + return { + tagName: element?.tagName ?? '', + hasOnlyText: element?.children?.length === 0 && + element?.innerText?.length > 0, + } + } + }, + { x: coordinates.x, y: coordinates.y }, + ); + return elementInfo; + } catch (error) { + const { message, stack } = error as Error; + logger.log('error', `Error while retrieving selector: ${message}`); + logger.log('error', `Stack: ${stack}`); + } +} + + From 45c51c0d32ffb286abffc728cded2623415c37b6 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Wed, 5 Jun 2024 07:05:53 +0530 Subject: [PATCH 08/83] docs: getElementInformation comment docs --- server/src/workflow-management/selector.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index 32363da2..66b1d74a 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -49,7 +49,14 @@ export const getRect = async (page: Page, coordinates: Coordinates) => { } } - +/** + * Checks the basic info about an element and returns a {@link BaseActionInfo} object. + * If the element is not found, returns undefined. + * @param page The page instance. + * @param coordinates Coordinates of an element. + * @category WorkflowManagement-Selectors + * @returns {Promise} + */ export const getElementInformation = async ( page: Page, coordinates: Coordinates From 28de16be12bf6abfc15e22e25f2fcd8e892b48b4 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Wed, 5 Jun 2024 10:34:17 +0530 Subject: [PATCH 09/83] feat: type Options for selctors --- server/src/workflow-management/selector.ts | 36 +++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index 66b1d74a..1e9db855 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -86,7 +86,41 @@ export const getElementInformation = async ( } } - +/** + * Returns the best and unique css {@link Selectors} for the element on the page. + * Internally uses a finder function from https://github.com/antonmedv/finder/blob/master/finder.ts + * available as a npm package: @medv/finder + * + * The finder needs to be executed and defined inside a browser context. Meaning, + * the code needs to be available inside a page evaluate function. + * @param page The page instance. + * @param coordinates Coordinates of an element. + * @category WorkflowManagement-Selectors + * @returns {Promise} + */ +export const getSelectors = async (page: Page, coordinates: Coordinates) => { + try { + const selectors : any = await page.evaluate(async ({ x, y }) => { + + type Options = { + root: Element; + idName: (name: string) => boolean; + className: (name: string) => boolean; + tagName: (name: string) => boolean; + attr: (name: string, value: string) => boolean; + seedMinLength: number; + optimizedMinLength: number; + threshold: number; + maxNumberOfTries: number; + }; + + + + +}; + + +} From a2d4dfb68cae46743742f9425b744d5c2053b0f1 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Wed, 5 Jun 2024 10:36:06 +0530 Subject: [PATCH 10/83] feat: implement finder for selector generation --- server/src/workflow-management/selector.ts | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index 1e9db855..ef2e0c25 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -115,9 +115,21 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { }; - + function finder(input: Element, options?: Partial) { -}; + const defaults: Options = { + root: document.body, + idName: (name: string) => true, + className: (name: string) => true, + tagName: (name: string) => true, + attr: (name: string, value: string) => false, + seedMinLength: 1, + optimizedMinLength: 2, + threshold: 1000, + maxNumberOfTries: 10000, + }; + + } } @@ -125,3 +137,4 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { + From 24bdea4c10f319dd388e8ef4b62c3f1337821819 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Wed, 5 Jun 2024 10:36:27 +0530 Subject: [PATCH 11/83] feat: find root document --- server/src/workflow-management/selector.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index ef2e0c25..b641aa8a 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -131,6 +131,15 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { } + function findRootDocument(rootNode: Element | Document, defaults: Options) { + if (rootNode.nodeType === Node.DOCUMENT_NODE) { + return rootNode; + } + if (rootNode === defaults.root) { + return rootNode.ownerDocument as Document; + } + return rootNode; + } } From 01f338128911314a06985830ee0563bce82750f7 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Wed, 5 Jun 2024 10:42:00 +0530 Subject: [PATCH 12/83] feat: types for Node --- server/src/workflow-management/selector.ts | 131 ++++++++++++++++++++- 1 file changed, 129 insertions(+), 2 deletions(-) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index b641aa8a..1a2400af 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -101,6 +101,22 @@ export const getElementInformation = async ( export const getSelectors = async (page: Page, coordinates: Coordinates) => { try { const selectors : any = await page.evaluate(async ({ x, y }) => { + // version @medv/finder + // https://github.com/antonmedv/finder/blob/master/finder.ts + + type Node = { + name: string; + penalty: number; + level?: number; + }; + + type Path = Node[]; + + enum Limit { + All, + Two, + One, + } type Options = { root: Element; @@ -114,9 +130,12 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { maxNumberOfTries: number; }; + let config: Options; + + let rootDocument: Document | Element; function finder(input: Element, options?: Partial) { - + const defaults: Options = { root: document.body, idName: (name: string) => true, @@ -129,6 +148,11 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { maxNumberOfTries: 10000, }; + config = { ...defaults, ...options }; + + rootDocument = findRootDocument(config.root, defaults); + + } function findRootDocument(rootNode: Element | Document, defaults: Options) { @@ -141,7 +165,110 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { return rootNode; } -} + function bottomUpSearch( + input: Element, + limit: Limit, + fallback?: () => Path | null + ): Path | null { + let path: Path | null = null; + let stack: Node[][] = []; + let current: Element | null = input; + let i = 0; + + while (current && current !== config.root.parentElement) { + let level: Node[] = maybe(id(current)) || + maybe(...attr(current)) || + maybe(...classNames(current)) || + maybe(tagName(current)) || [any()]; + + const nth = index(current); + + if (limit === Limit.All) { + if (nth) { + level = level.concat( + level.filter(dispensableNth).map((node) => nthChild(node, nth)) + ); + } + } else if (limit === Limit.Two) { + level = level.slice(0, 1); + + if (nth) { + level = level.concat( + level.filter(dispensableNth).map((node) => nthChild(node, nth)) + ); + } + } else if (limit === Limit.One) { + const [node] = (level = level.slice(0, 1)); + + if (nth && dispensableNth(node)) { + level = [nthChild(node, nth)]; + } + } + + for (let node of level) { + node.level = i; + } + + stack.push(level); + + if (stack.length >= config.seedMinLength) { + path = findUniquePath(stack, fallback); + if (path) { + break; + } + } + + current = current.parentElement; + i++; + } + + if (!path) { + path = findUniquePath(stack, fallback); + } + + return path; + } + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +}; + + + From ebd82480e1da1dde33278c03756d0b421f623b80 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Wed, 5 Jun 2024 10:44:01 +0530 Subject: [PATCH 13/83] feat: throw err for non-element node type --- server/src/workflow-management/selector.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index 1a2400af..69c79f15 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -135,7 +135,11 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { let rootDocument: Document | Element; function finder(input: Element, options?: Partial) { - + if (input.nodeType !== Node.ELEMENT_NODE) { + throw new Error(`Can't generate CSS selector for non-element node type.`); + } + + const defaults: Options = { root: document.body, idName: (name: string) => true, From 4dd35d599b2bb160846aa19ffab790ab10505749 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Wed, 5 Jun 2024 10:44:19 +0530 Subject: [PATCH 14/83] feat: html handle --- server/src/workflow-management/selector.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index 69c79f15..89da7f2d 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -139,6 +139,9 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { throw new Error(`Can't generate CSS selector for non-element node type.`); } + if ('html' === input.tagName.toLowerCase()) { + return 'html'; + } const defaults: Options = { root: document.body, From 1afd38f1c04b32d4970e8ccaf0b5ab7f18e4230d Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Wed, 5 Jun 2024 10:45:15 +0530 Subject: [PATCH 15/83] feat: use bottomUpSearch for path --- server/src/workflow-management/selector.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index 89da7f2d..b10e644e 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -159,7 +159,10 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { rootDocument = findRootDocument(config.root, defaults); - + let path = bottomUpSearch(input, Limit.All, () => + bottomUpSearch(input, Limit.Two, () => bottomUpSearch(input, Limit.One)) + ); + } function findRootDocument(rootNode: Element | Document, defaults: Options) { From ee624361ffb54167d935beb7475c2e7fbb40d74d Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Wed, 5 Jun 2024 10:45:51 +0530 Subject: [PATCH 16/83] feat: if path, sort, optimize path & input --- server/src/workflow-management/selector.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index b10e644e..2b8b0d1f 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -163,6 +163,17 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { bottomUpSearch(input, Limit.Two, () => bottomUpSearch(input, Limit.One)) ); + if (path) { + const optimized = sort(optimize(path, input)); + + if (optimized.length > 0) { + path = optimized[0]; + } + + return selector(path); + } else { + throw new Error(`Selector was not found.`); + } } function findRootDocument(rootNode: Element | Document, defaults: Options) { From 4e2c80d0564defcf7fb449755e61bd9154657a5d Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Wed, 5 Jun 2024 11:41:56 +0530 Subject: [PATCH 17/83] feat: bottom up search by traversing the DOM tree upwards from the element for CSS selector --- server/src/workflow-management/selector.ts | 36 ---------------------- 1 file changed, 36 deletions(-) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index 2b8b0d1f..5f8a74e9 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -250,42 +250,6 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { return path; } - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - }; From 76ff3392d3e55a5e35a1e7c8a96dee12bf6d2b93 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Wed, 5 Jun 2024 22:16:18 +0530 Subject: [PATCH 18/83] feat: sort all possible combinations of selectors from the stack --- server/src/workflow-management/selector.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index 5f8a74e9..a14552a7 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -250,6 +250,12 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { return path; } + function findUniquePath( + stack: Node[][], + fallback?: () => Path | null + ): Path | null { + const paths = sort(combinations(stack)); + }; From 6dda179409e3393d88421ba344a3ea19bdbc2d27 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Wed, 5 Jun 2024 22:19:52 +0530 Subject: [PATCH 19/83] feat: use fallback if too many combinations found --- server/src/workflow-management/selector.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index a14552a7..7cefa35f 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -255,6 +255,14 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { fallback?: () => Path | null ): Path | null { const paths = sort(combinations(stack)); + + if (paths.length > config.threshold) { + return fallback ? fallback() : null; + } + + return null; + } + }; From 1eeb73b93e707e9c880969593357c9ceeceaab59 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Wed, 5 Jun 2024 22:27:41 +0530 Subject: [PATCH 20/83] feat: try each candidate path to see if it uniquely identifies an element on the webpage --- server/src/workflow-management/selector.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index 7cefa35f..b77a7b3b 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -260,6 +260,12 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { return fallback ? fallback() : null; } + for (let candidate of paths) { + if (unique(candidate)) { + + } + } + return null; } From 12619948996eb28dfa088b9571ce13b40cab1f54 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Wed, 5 Jun 2024 22:28:15 +0530 Subject: [PATCH 21/83] feat: return first unique path found --- server/src/workflow-management/selector.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index b77a7b3b..68490dfb 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -262,7 +262,7 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { for (let candidate of paths) { if (unique(candidate)) { - + return candidate; } } From 7ba43ec3d7237a0f31ca1aee3d5f2e69a99184b7 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Wed, 5 Jun 2024 22:31:05 +0530 Subject: [PATCH 22/83] feat: start with the first element in the path --- server/src/workflow-management/selector.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index 68490dfb..0ff8625c 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -269,6 +269,11 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { return null; } + function selector(path: Path): string { + let node = path[0]; + + } + }; From 8e2b0b4550467cbceb139c5787a64f107a8061e0 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Wed, 5 Jun 2024 22:31:33 +0530 Subject: [PATCH 23/83] feat: intialize query string with first query name --- server/src/workflow-management/selector.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index 0ff8625c..ca65921b 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -271,7 +271,8 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { function selector(path: Path): string { let node = path[0]; - + let query = node.name; + } From f3a1e7b9f68bb62cd7d5f9dec9d78c4a45357b3d Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Wed, 5 Jun 2024 22:32:19 +0530 Subject: [PATCH 24/83] feat: get the element's level (depth in DOM tree) --- server/src/workflow-management/selector.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index ca65921b..6f87c1bf 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -272,7 +272,12 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { function selector(path: Path): string { let node = path[0]; let query = node.name; - + for (let i = 1; i < path.length; i++) { + const level = path[i].level || 0; + + + } + } From 223f09f93dda3af3ab469beefdf515991a26df07 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Wed, 5 Jun 2024 22:33:06 +0530 Subject: [PATCH 25/83] feat: child selector (direct descendant) --- server/src/workflow-management/selector.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index 6f87c1bf..6761f7b4 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -275,9 +275,11 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { for (let i = 1; i < path.length; i++) { const level = path[i].level || 0; - + if (node.level === level - 1) { + query = `${path[i].name} > ${query}`; + } } - + } From 3d96faf9ac661c74093eaa883894c5a98432b583 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Wed, 5 Jun 2024 22:33:41 +0530 Subject: [PATCH 26/83] feat: sibling / ancestor --- server/src/workflow-management/selector.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index 6761f7b4..36fcbd7d 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -277,9 +277,13 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { if (node.level === level - 1) { query = `${path[i].name} > ${query}`; - } + } else { + query = `${path[i].name} ${query}`; + } + + } - + } From 82a447cd5df3e26405ee2c8ffcc3757409677cbf Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Wed, 5 Jun 2024 22:34:02 +0530 Subject: [PATCH 27/83] feat: move on to next el in the path --- server/src/workflow-management/selector.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index 36fcbd7d..4855e979 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -281,9 +281,9 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { query = `${path[i].name} ${query}`; } - + node = path[i]; } - + return query; } From 40100eb495c359a1f81611330737b8918328d7e5 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Wed, 5 Jun 2024 22:36:53 +0530 Subject: [PATCH 28/83] feat: calculates a penalty score based on a CSS selector pat. --- server/src/workflow-management/selector.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index 4855e979..a05957df 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -286,6 +286,10 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { return query; } + function penalty(path: Path): number { + return path.map((node) => node.penalty).reduce((acc, i) => acc + i, 0); + } + }; From 139c22ba83fb8447d4e1068b882ebf2d3963adf0 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Wed, 5 Jun 2024 22:48:55 +0530 Subject: [PATCH 29/83] feat: check if a CSS selector path identifies a unique el on webpage --- server/src/workflow-management/selector.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index a05957df..a8c45ecf 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -290,6 +290,19 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { return path.map((node) => node.penalty).reduce((acc, i) => acc + i, 0); } + function unique(path: Path) { + switch (rootDocument.querySelectorAll(selector(path)).length) { + case 0: + throw new Error( + `Can't select any node with this selector: ${selector(path)}` + ); + case 1: + return true; + default: + return false; + } + } + }; From fe39d8620f69a0fe0d16c11fbc6e1f7c740301f0 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Wed, 5 Jun 2024 23:19:17 +0530 Subject: [PATCH 30/83] feat: check if el has unique id --- server/src/workflow-management/selector.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index a8c45ecf..30940bbd 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -303,6 +303,14 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { } } + function id(input: Element): Node | null { + const elementId = input.getAttribute('id'); + + return null; + } + + + }; From 66ad8e9c28039c918a8b9c64aadd9c0cb00db2fd Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Wed, 5 Jun 2024 23:19:40 +0530 Subject: [PATCH 31/83] feat: check if id valid as per idName --- server/src/workflow-management/selector.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index 30940bbd..f3f77e2e 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -305,7 +305,12 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { function id(input: Element): Node | null { const elementId = input.getAttribute('id'); - + if (elementId && config.idName(elementId)) { + return { + name: '#' + cssesc(elementId, { isIdentifier: true }), + penalty: 0, + }; + } return null; } From cf3b56d343b35cce0eb09e8d03a87fab13b6bc89 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Wed, 5 Jun 2024 23:20:34 +0530 Subject: [PATCH 32/83] feat: retrive attribute selectors for el --- server/src/workflow-management/selector.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index f3f77e2e..9ef63985 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -314,6 +314,14 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { return null; } + function attr(input: Element): Node[] { + const attrs = Array.from(input.attributes).filter((attr) => + config.attr(attr.name, attr.value) + ); + + + } + From aeed80ee8d40896ee81dbf8de72646f949d703a3 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Wed, 5 Jun 2024 23:21:48 +0530 Subject: [PATCH 33/83] feat: create selector nodes for each valid attr --- server/src/workflow-management/selector.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index 9ef63985..82e4c897 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -319,7 +319,17 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { config.attr(attr.name, attr.value) ); - + return attrs.map( + (attr): Node => ({ + name: + '[' + + cssesc(attr.name, { isIdentifier: true }) + + '="' + + cssesc(attr.value) + + '"]', + penalty: 0.5, + }) + ); } From 16e617405e06e41097d45076eed529b361bb8856 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Wed, 5 Jun 2024 23:22:34 +0530 Subject: [PATCH 34/83] feat: retrive class selectors for el --- server/src/workflow-management/selector.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index 82e4c897..ef34dd26 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -332,8 +332,13 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { ); } - + function classNames(input: Element): Node[] { + const names = Array.from(input.classList).filter(config.className); + + } + + }; From aba844e4a4936f28c50c0607a2b28a5af5bef1ce Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Wed, 5 Jun 2024 23:22:59 +0530 Subject: [PATCH 35/83] feat: create selector nodes for each valid class name --- server/src/workflow-management/selector.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index ef34dd26..942dd3dd 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -335,7 +335,12 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { function classNames(input: Element): Node[] { const names = Array.from(input.classList).filter(config.className); - + return names.map( + (name): Node => ({ + name: '.' + cssesc(name, { isIdentifier: true }), + penalty: 1, + }) + ); } From 161af107ecc2b02a7c63a7bbbb18c1392fa95680 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Wed, 5 Jun 2024 23:23:38 +0530 Subject: [PATCH 36/83] feat: check if el tag name valid --- server/src/workflow-management/selector.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index 942dd3dd..1a025515 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -343,7 +343,19 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { ); } + function tagName(input: Element): Node | null { + const name = input.tagName.toLowerCase(); + if (config.tagName(name)) { + return { + name, + penalty: 2, + }; + } + return null; + } + + }; From e75216634325f4e609e6c608b101a647156592ae Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Wed, 5 Jun 2024 23:24:38 +0530 Subject: [PATCH 37/83] feat: return selector node representing wildcard selector with highest degree penalty 3 --- server/src/workflow-management/selector.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index 1a025515..94b6269a 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -354,9 +354,15 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { return null; } - + function any(): Node { + return { + name: '*', + penalty: 3, + }; + } + }; From ae4d58ab0b302c1871b40674133478a33e4bbbc0 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Wed, 5 Jun 2024 23:25:31 +0530 Subject: [PATCH 38/83] feat: calculate els position among siblings (assume parent el) --- server/src/workflow-management/selector.ts | 27 ++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index 94b6269a..d1c5c17d 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -361,6 +361,33 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { }; } + function index(input: Element): number | null { + const parent = input.parentNode; + if (!parent) { + return null; + } + + let child = parent.firstChild; + if (!child) { + return null; + } + + let i = 0; + while (child) { + if (child.nodeType === Node.ELEMENT_NODE) { + i++; + } + + if (child === input) { + break; + } + + child = child.nextSibling; + } + + return i; + } + }; From 37a1879c938cbbfdf85bdbc2b5123b0b2fad966a Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Wed, 5 Jun 2024 23:26:56 +0530 Subject: [PATCH 39/83] feat: create selector node representing :nth-chil()d pseudo-class --- server/src/workflow-management/selector.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index d1c5c17d..300915bc 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -388,6 +388,13 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { return i; } + function nthChild(node: Node, i: number): Node { + return { + name: node.name + `:nth-child(${i})`, + + }; + } + }; From d5f1fcffdd9efa3522cbee3bc0c1076c8d1cf992 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Wed, 5 Jun 2024 23:27:20 +0530 Subject: [PATCH 40/83] feat: increase penalty compared to base node --- server/src/workflow-management/selector.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index 300915bc..2da8dc7e 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -391,7 +391,7 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { function nthChild(node: Node, i: number): Node { return { name: node.name + `:nth-child(${i})`, - + penalty: node.penalty + 1, }; } From 531a2ba6bdc51ff779f6899fbcba9600b8720cba Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Wed, 5 Jun 2024 23:28:11 +0530 Subject: [PATCH 41/83] feat: check if ::nth-child() psuedo-class selector be omitted --- server/src/workflow-management/selector.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index 2da8dc7e..02c3fee1 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -395,7 +395,12 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { }; } + function dispensableNth(node: Node) { + return node.name !== 'html' && !node.name.startsWith('#'); + } + + }; From 4af7caf0aa54ab9ab0f08ba1a40e2dd81ee0bc25 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Wed, 5 Jun 2024 23:38:36 +0530 Subject: [PATCH 42/83] feat: filter null/undefined vals from potential selector nodes --- server/src/workflow-management/selector.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index 02c3fee1..af7227e5 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -399,9 +399,16 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { return node.name !== 'html' && !node.name.startsWith('#'); } - + function maybe(...level: (Node | null)[]): Node[] | null { + const list = level.filter(notEmpty); + if (list.length > 0) { + return list; + } + return null; + } + }; From 0dd469eae637ea4bccbcb3f273a2bf776e3e7460 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Wed, 5 Jun 2024 23:39:55 +0530 Subject: [PATCH 43/83] feat: helper fxn to check null vals --- server/src/workflow-management/selector.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index af7227e5..77df25bf 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -407,7 +407,12 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { return null; } + function notEmpty(value: T | null | undefined): value is T { + return value !== null && value !== undefined; + } + + }; From 134b29ecb920a6ab792078c5233d52acccf7de25 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Wed, 5 Jun 2024 23:42:27 +0530 Subject: [PATCH 44/83] feat: recursively generate all possible combinations of selector nodes fromprovided stack --- server/src/workflow-management/selector.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index 77df25bf..8beac10c 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -411,6 +411,16 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { return value !== null && value !== undefined; } + function* combinations(stack: Node[][], path: Node[] = []): Generator { + if (stack.length > 0) { + for (let node of stack[0]) { + yield* combinations(stack.slice(1, stack.length), path.concat(node)); + } + } else { + yield path; + } + } + From 710405024bce00cc37c7015669da4723cd42d2f0 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Wed, 5 Jun 2024 23:43:00 +0530 Subject: [PATCH 45/83] feat: sort a list of selector paths based on their penalty scores --- server/src/workflow-management/selector.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index 8beac10c..939d618a 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -421,7 +421,9 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { } } - + function sort(paths: Iterable): Path[] { + return Array.from(paths).sort((a, b) => penalty(a) - penalty(b)); + } }; From 6d3ba935c10f5365369a99b419c6f0ff2b71a757 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Thu, 6 Jun 2024 00:25:49 +0530 Subject: [PATCH 46/83] feat: type Scope to keep track of info during optimization --- server/src/workflow-management/selector.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index 939d618a..898dab0b 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -425,6 +425,12 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { return Array.from(paths).sort((a, b) => penalty(a) - penalty(b)); } + type Scope = { + counter: number; + visited: Map; + }; + + }; From fff6149fef1d75da0c90edb1b18bc32cfc845a55 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Thu, 6 Jun 2024 00:27:18 +0530 Subject: [PATCH 47/83] feat: check if provided CSS sel path selects the same el as input el --- server/src/workflow-management/selector.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index 898dab0b..6023e9e2 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -430,7 +430,11 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { visited: Map; }; - + + function same(path: Path, input: Element) { + return rootDocument.querySelector(selector(path)) === input; + } + }; From ad61b2a0b103d8eda6c217ab2383d63cd60ec8a2 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Thu, 6 Jun 2024 00:28:15 +0530 Subject: [PATCH 48/83] feat: generator fxn to optimize given CSS selector path --- server/src/workflow-management/selector.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index 6023e9e2..54a0b67e 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -430,6 +430,16 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { visited: Map; }; + function* optimize( + path: Path, + input: Element, + scope: Scope = { + counter: 0, + visited: new Map(), + } + ): Generator { + + } function same(path: Path, input: Element) { return rootDocument.querySelector(selector(path)) === input; From 36c0af28c583268d248e0159d246272a54cdf85d Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Thu, 6 Jun 2024 00:30:48 +0530 Subject: [PATCH 49/83] feat: check if path len is longer than 2 els & meets min optimization len --- server/src/workflow-management/selector.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index 54a0b67e..2efdded9 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -438,7 +438,9 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { visited: new Map(), } ): Generator { - + if (path.length > 2 && path.length > config.optimizedMinLength) { + + } } function same(path: Path, input: Element) { From 4efd17740642cceab291aa5df9e1ce554899b24c Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Thu, 6 Jun 2024 00:31:56 +0530 Subject: [PATCH 50/83] feat: loop through each el in the path except the first and last --- server/src/workflow-management/selector.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index 2efdded9..79e90eb5 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -439,7 +439,13 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { } ): Generator { if (path.length > 2 && path.length > config.optimizedMinLength) { - + for (let i = 1; i < path.length - 1; i++) { + if (scope.counter > config.maxNumberOfTries) { + return; // Okay At least I tried! + } + scope.counter += 1; + + } } } From cc7cd09b03b5cc11655d16320caf047554d26d6c Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Thu, 6 Jun 2024 00:32:46 +0530 Subject: [PATCH 51/83] feat: create a copy of the path to avoid og modification --- server/src/workflow-management/selector.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index 79e90eb5..70b6c628 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -444,6 +444,7 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { return; // Okay At least I tried! } scope.counter += 1; + const newPath = [...path]; } } From 27e9945395d201ad74994f29a86f0e2203c9d36d Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Thu, 6 Jun 2024 00:33:49 +0530 Subject: [PATCH 52/83] feat: remove el at the current index from the copy --- server/src/workflow-management/selector.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index 70b6c628..05fd6a88 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -445,6 +445,8 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { } scope.counter += 1; const newPath = [...path]; + newPath.splice(i, 1); + } } From 28963cb96fee2df81aedb51d537dca5e3aa34136 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Thu, 6 Jun 2024 00:34:17 +0530 Subject: [PATCH 53/83] feat: Convert the modified path to a selector string --- server/src/workflow-management/selector.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index 05fd6a88..a1b244aa 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -446,7 +446,7 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { scope.counter += 1; const newPath = [...path]; newPath.splice(i, 1); - + const newPathKey = selector(newPath); } } From e0072bdff9e8d4c015d4ba25d0672a110a11799e Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Thu, 6 Jun 2024 00:34:56 +0530 Subject: [PATCH 54/83] feat: skip if this modified path has already been explored --- server/src/workflow-management/selector.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index a1b244aa..19b5567c 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -447,6 +447,9 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { const newPath = [...path]; newPath.splice(i, 1); const newPathKey = selector(newPath); + if (scope.visited.has(newPathKey)) { + return; + } } } From ff3687c1fddc0b5478de55abc2d546ec443409c4 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Thu, 6 Jun 2024 00:35:21 +0530 Subject: [PATCH 55/83] feat: check if the modified path still uniquely identifies the element: --- server/src/workflow-management/selector.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index 19b5567c..3d440963 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -450,7 +450,11 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { if (scope.visited.has(newPathKey)) { return; } - + if (unique(newPath) && same(newPath, input)) { + yield newPath; + scope.visited.set(newPathKey, true); + yield* optimize(newPath, input, scope); + } } } } From 88e6a53c6e3f70c9f19bfbabdd6dd185082ed8f1 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Thu, 6 Jun 2024 00:52:04 +0530 Subject: [PATCH 56/83] feat: regex for CSS selectors --- server/src/workflow-management/selector.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index 3d440963..9d2cfe81 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -463,6 +463,11 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { return rootDocument.querySelector(selector(path)) === input; } + const regexAnySingleEscape = /[ -,\.\/:-@\[-\^`\{-~]/; + const regexSingleEscape = /[ -,\.\/:-@\[\]\^`\{-~]/; + const regexExcessiveSpaces = + /(^|\\+)?(\\[A-F0-9]{1,6})\x20(?![a-fA-F0-9\x20])/g; + }; From 52d74f9ea4e6e1638bb6d1239832508d08edcbd4 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Thu, 6 Jun 2024 01:23:26 +0530 Subject: [PATCH 57/83] feat: validate quotes to be single or double --- server/src/workflow-management/selector.ts | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index 9d2cfe81..61fdc92a 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -468,6 +468,26 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { const regexExcessiveSpaces = /(^|\\+)?(\\[A-F0-9]{1,6})\x20(?![a-fA-F0-9\x20])/g; + const defaultOptions = { + escapeEverything: false, + isIdentifier: false, + quotes: 'single', + wrap: false, + }; + + function cssesc(string: string, opt: Partial = {}) { + const options = { ...defaultOptions, ...opt }; + if (options.quotes != 'single' && options.quotes != 'double') { + options.quotes = 'single'; + } + const quote = options.quotes == 'double' ? '"' : "'"; + const isIdentifier = options.isIdentifier; + + + } + + + }; @@ -477,4 +497,3 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { - From e241a7a99b46c218a20e6183f7b97904a375de5f Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Thu, 6 Jun 2024 01:24:50 +0530 Subject: [PATCH 58/83] feat: ASCII & alphanumeric characters --- server/src/workflow-management/selector.ts | 47 +++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index 61fdc92a..5bfd3e68 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -483,7 +483,52 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { const quote = options.quotes == 'double' ? '"' : "'"; const isIdentifier = options.isIdentifier; - + const firstChar = string.charAt(0); + let output = ''; + let counter = 0; + const length = string.length; + while (counter < length) { + const character = string.charAt(counter++); + let codePoint = character.charCodeAt(0); + let value: string | undefined = void 0; + // If it’s not a printable ASCII character… + if (codePoint < 0x20 || codePoint > 0x7e) { + if (codePoint >= 0xd800 && codePoint <= 0xdbff && counter < length) { + // It’s a high surrogate, and there is a next character. + const extra = string.charCodeAt(counter++); + if ((extra & 0xfc00) == 0xdc00) { + // next character is low surrogate + codePoint = ((codePoint & 0x3ff) << 10) + (extra & 0x3ff) + 0x10000; + } else { + // It’s an unmatched surrogate; only append this code unit, in case + // the next code unit is the high surrogate of a surrogate pair. + counter--; + } + } + value = '\\' + codePoint.toString(16).toUpperCase() + ' '; + } else { + if (options.escapeEverything) { + if (regexAnySingleEscape.test(character)) { + value = '\\' + character; + } else { + value = '\\' + codePoint.toString(16).toUpperCase() + ' '; + } + } else if (/[\t\n\f\r\x0B]/.test(character)) { + value = '\\' + codePoint.toString(16).toUpperCase() + ' '; + } else if ( + character == '\\' || + (!isIdentifier && + ((character == '"' && quote == character) || + (character == "'" && quote == character))) || + (isIdentifier && regexSingleEscape.test(character)) + ) { + value = '\\' + character; + } else { + value = character; + } + } + output += value; + } } From 358efd6dea75deb328111e1683ae012845d5b142 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Thu, 6 Jun 2024 01:25:44 +0530 Subject: [PATCH 59/83] feat: handle edge cases for valid selectors starting with hyphen or numbers --- server/src/workflow-management/selector.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index 5bfd3e68..c211669b 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -529,6 +529,14 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { } output += value; } + + if (isIdentifier) { + if (/^-[-\d]/.test(output)) { + output = '\\-' + output.slice(1); + } else if (/\d/.test(firstChar)) { + output = '\\3' + firstChar + ' ' + output.slice(1); + } + } } From e7a3d146df0b15c500f9bd85fa7b4b509bad9c39 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Thu, 6 Jun 2024 01:26:33 +0530 Subject: [PATCH 60/83] feat: remove spaces after \HEX escapes that are not followed by a hex digit --- server/src/workflow-management/selector.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index c211669b..4644a09a 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -537,6 +537,23 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { output = '\\3' + firstChar + ' ' + output.slice(1); } } + + // Remove spaces after `\HEX` escapes that are not followed by a hex digit, + // since they’re redundant. Note that this is only possible if the escape + // sequence isn’t preceded by an odd number of backslashes. + output = output.replace(regexExcessiveSpaces, function ($0, $1, $2) { + if ($1 && $1.length % 2) { + // It’s not safe to remove the space, so don’t. + return $0; + } + // Strip the space. + return ($1 || '') + $2; + }); + + if (!isIdentifier && options.wrap) { + return quote + output + quote; + } + return output; } From 90de043ba50a06253eb470bd8bd353263a197001 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Thu, 6 Jun 2024 05:00:16 +0530 Subject: [PATCH 61/83] feat: generate CSS selectors targeting a given HTML el based on its tag name, class names, attributes, & potential test IDs --- server/src/workflow-management/selector.ts | 68 ++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index 4644a09a..22c02814 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -558,7 +558,75 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { + const genSelectors = (element: HTMLElement | null) =>{ + if (element == null) { + return null; + } + + const href = element.getAttribute('href'); + + let generalSelector = null; + try { + generalSelector = finder(element); + } catch (e) { + } + + let attrSelector = null; + try { + attrSelector = finder(element, { attr: () => true }); + } catch (e) { + } + + const hrefSelector = genSelectorForAttributes(element, ['href']); + const formSelector = genSelectorForAttributes(element, [ + 'name', + 'placeholder', + 'for', + ]); + const accessibilitySelector = genSelectorForAttributes(element, [ + 'aria-label', + 'alt', + 'title', + ]); + + const testIdSelector = genSelectorForAttributes(element, [ + 'data-testid', + 'data-test-id', + 'data-testing', + 'data-test', + 'data-qa', + 'data-cy', + ]); + + // We won't use an id selector if the id is invalid (starts with a number) + let idSelector = null; + try { + idSelector = + isAttributesDefined(element, ['id']) && + !isCharacterNumber(element.id?.[0]) + ? // Certain apps don't have unique ids (ex. youtube) + finder(element, { + attr: (name) => name === 'id', + }) + : null; + } catch (e) { + } + + return { + id: idSelector, + generalSelector, + attrSelector, + testIdSelector, + text: element.innerText, + href, + // Only try to pick an href selector if there is an href on the element + hrefSelector, + accessibilitySelector, + formSelector, + }; + } + }; From 67827f920299e724545daa1958ce3c560722c7f4 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Thu, 6 Jun 2024 05:03:43 +0530 Subject: [PATCH 62/83] feat: create a new Set object to store unique attribute values --- server/src/workflow-management/selector.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index 22c02814..e811db3f 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -625,8 +625,14 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { formSelector, }; } - + function genAttributeSet(element: HTMLElement, attributes: string[]) { + return new Set( + + ); + } + + }; @@ -635,3 +641,4 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { + From 943e718130aa45e03864a981d6b8198c706ad22f Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Thu, 6 Jun 2024 05:04:32 +0530 Subject: [PATCH 63/83] feat: filter attributes --- server/src/workflow-management/selector.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index e811db3f..3cf8bb34 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -628,7 +628,10 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { function genAttributeSet(element: HTMLElement, attributes: string[]) { return new Set( - + attributes.filter((attr) => { + const attrValue = element.getAttribute(attr); + + }) ); } From 81faceb2f7823ab5507ee163a98ea1ec676f6bf8 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Thu, 6 Jun 2024 05:05:10 +0530 Subject: [PATCH 64/83] feat: add attribute value iff val !null & len > 0 --- server/src/workflow-management/selector.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index 3cf8bb34..38905a1a 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -630,7 +630,7 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { return new Set( attributes.filter((attr) => { const attrValue = element.getAttribute(attr); - + return attrValue != null && attrValue.length > 0; }) ); } From 23a96b8118169930d11e4d89f9ba374f95ad30c4 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Thu, 6 Jun 2024 05:07:03 +0530 Subject: [PATCH 65/83] feat: check if attribute defined --- server/src/workflow-management/selector.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index 38905a1a..aaed5c73 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -635,7 +635,11 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { ); } - + function isAttributesDefined(element: HTMLElement, attributes: string[]) { + return genAttributeSet(element, attributes).size > 0; + } + + }; From f5af4100646478952da79d4dba1c651b756baadb Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Thu, 6 Jun 2024 05:07:59 +0530 Subject: [PATCH 66/83] feat: get all attributed that are !null --- server/src/workflow-management/selector.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index aaed5c73..e15f43d5 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -639,7 +639,14 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { return genAttributeSet(element, attributes).size > 0; } +// Gets all attributes that aren't null and empty + function genValidAttributeFilter(element: HTMLElement, attributes: string[]) { + const attrSet = genAttributeSet(element, attributes); + return (name: string) => attrSet.has(name); + } + + }; From 71cd95a1a2a1b997fc84434aed03f6acad97edef Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Thu, 6 Jun 2024 05:08:57 +0530 Subject: [PATCH 67/83] feat: generate selector for attributes --- server/src/workflow-management/selector.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index e15f43d5..ff6c0e8e 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -646,7 +646,21 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { return (name: string) => attrSet.has(name); } - + function genSelectorForAttributes(element: HTMLElement, attributes: string[]) { + let selector = null; + try { + selector = isAttributesDefined(element, attributes) + ? finder(element, { + idName: () => false, + attr: genValidAttributeFilter(element, attributes), + }) + : null; + } catch (e) {} + + return selector; + } + + }; From 1aa120120ef09f64925b6829c609f7024f8e6433 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Thu, 6 Jun 2024 05:09:26 +0530 Subject: [PATCH 68/83] feat: don't use id for selector --- server/src/workflow-management/selector.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index ff6c0e8e..3d67f1da 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -651,7 +651,7 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { try { selector = isAttributesDefined(element, attributes) ? finder(element, { - idName: () => false, + idName: () => false, // Don't use the id to generate a selector attr: genValidAttributeFilter(element, attributes), }) : null; From a40ed23275742938120cfb4588e25448a6b58518 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Thu, 6 Jun 2024 05:09:52 +0530 Subject: [PATCH 69/83] feat: check if char is num --- server/src/workflow-management/selector.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index 3d67f1da..2a79572c 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -660,7 +660,12 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { return selector; } +// isCharacterNumber + function isCharacterNumber(char: string) { + return char.length === 1 && char.match(/[0-9]/); + } + }; From 669309938582ca3273811a6d5d901406b8e5345e Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Thu, 6 Jun 2024 05:11:37 +0530 Subject: [PATCH 70/83] feat: retrieve elements under cursor --- server/src/workflow-management/selector.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index 2a79572c..58753b8c 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -665,7 +665,19 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { return char.length === 1 && char.match(/[0-9]/); } - + const hoveredElement = document.elementFromPoint(x, y) as HTMLElement; + if ( + hoveredElement != null && + !hoveredElement.closest('#overlay-controls') != null + ) { + const { parentElement } = hoveredElement; + // Match the logic in recorder.ts for link clicks + const element = parentElement?.tagName === 'A' ? parentElement : hoveredElement; + const generatedSelectors = genSelectors(element); + return generatedSelectors; + } + }, { x: coordinates.x, y: coordinates.y }); + } }; From e03cfaf3895f4e6fdfc5ec0595bb205e4e643578 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Thu, 6 Jun 2024 05:11:59 +0530 Subject: [PATCH 71/83] fix: return selectors --- server/src/workflow-management/selector.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index 58753b8c..ae615521 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -677,6 +677,7 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { return generatedSelectors; } }, { x: coordinates.x, y: coordinates.y }); + return selectors; } }; From b27c45fc21297d15b1b71b6ad6ef365702201658 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Thu, 6 Jun 2024 05:12:14 +0530 Subject: [PATCH 72/83] fix: error handling --- server/src/workflow-management/selector.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index ae615521..61ad07b9 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -678,7 +678,12 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { } }, { x: coordinates.x, y: coordinates.y }); return selectors; - } + } catch (e) { + const { message, stack } = e as Error; + logger.log('error', `Error while retrieving element: ${message}`); + logger.log('error', `Stack: ${stack}`); + } + return null; }; From 0263d0789d16cbbef956fe879245b379edfd3ce6 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Thu, 6 Jun 2024 05:13:19 +0530 Subject: [PATCH 73/83] feat: selector already in workflow --- server/src/workflow-management/selector.ts | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index 61ad07b9..79bbbc07 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -686,9 +686,24 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => { return null; }; - - - +/** + * Returns the first pair from the given workflow that contains the given selector + * inside the where condition, and it is the only selector there. + * If a match is not found, returns undefined. + * @param selector The selector to find. + * @param workflow The workflow to search in. + * @category WorkflowManagement + * @returns {Promise} + */ +export const selectorAlreadyInWorkflow = (selector: string, workflow: Workflow) => { + return workflow.find((pair: WhereWhatPair) => { + if (pair.where.selectors?.includes(selector)) { + if (pair.where.selectors?.length === 1) { + return pair; + } + } + }); +}; From cded3fead6c378a59b21104e7b2213a28b3ff46f Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Thu, 6 Jun 2024 05:13:48 +0530 Subject: [PATCH 74/83] feat: check if given selectors are visible on page at same time --- server/src/workflow-management/selector.ts | 37 ++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index 79bbbc07..afc990ea 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -705,6 +705,43 @@ export const selectorAlreadyInWorkflow = (selector: string, workflow: Workflow) }); }; +/** + * Checks whether the given selectors are visible on the page at the same time. + * @param selectors The selectors to check. + * @param page The page to use for the validation. + * @category WorkflowManagement + */ +export const isRuleOvershadowing = async(selectors: string[], page:Page): Promise => { + for (const selector of selectors){ + const areElsVisible = await page.$$eval(selector, + (elems) => { + const isVisible = ( elem: HTMLElement | SVGElement ) => { + if (elem instanceof HTMLElement) { + return !!(elem.offsetWidth + || elem.offsetHeight + || elem.getClientRects().length + && window.getComputedStyle(elem).visibility !== "hidden"); + } else { + return !!(elem.getClientRects().length + && window.getComputedStyle(elem).visibility !== "hidden"); + } + }; + + const visibility: boolean[] = []; + elems.forEach((el) => visibility.push(isVisible(el))) + return visibility; + }) + if (areElsVisible.length === 0) { + return false + } + + if (areElsVisible.includes(false)){ + return false; + } + } + return true; +} + From bdc37a188f933f59acbe9ccad18ef929e4880343 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Thu, 6 Jun 2024 05:14:18 +0530 Subject: [PATCH 75/83] chore:: lint --- server/src/workflow-management/selector.ts | 1213 ++++++++++---------- 1 file changed, 607 insertions(+), 606 deletions(-) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index afc990ea..47f348b0 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -21,24 +21,25 @@ export const getRect = async (page: Page, coordinates: Coordinates) => { async ({ x, y }) => { const el = document.elementFromPoint(x, y) as HTMLElement; if (el) { - const { parentElement } = el; - // Match the logic in recorder.ts for link clicks - const element = parentElement?.tagName === 'A' ? parentElement : el; - const rectangle = element?.getBoundingClientRect(); - // @ts-ignore - if (rectangle) { - return { - x: rectangle.x, - y: rectangle.y, - width: rectangle.width, - height: rectangle.height, - top: rectangle.top, - right: rectangle.right, - bottom: rectangle.bottom, - left: rectangle.left, - }; + const { parentElement } = el; + // Match the logic in recorder.ts for link clicks + const element = parentElement?.tagName === 'A' ? parentElement : el; + const rectangle = element?.getBoundingClientRect(); + // @ts-ignore + if (rectangle) { + return { + x: rectangle.x, + y: rectangle.y, + width: rectangle.width, + height: rectangle.height, + top: rectangle.top, + right: rectangle.right, + bottom: rectangle.bottom, + left: rectangle.left, + }; + } } - }}, + }, { x: coordinates.x, y: coordinates.y }, ); return rect; @@ -65,7 +66,7 @@ export const getElementInformation = async ( const elementInfo = await page.evaluate( async ({ x, y }) => { const el = document.elementFromPoint(x, y) as HTMLElement; - if ( el ) { + if (el) { const { parentElement } = el; // Match the logic in recorder.ts for link clicks const element = parentElement?.tagName === 'A' ? parentElement : el; @@ -100,590 +101,590 @@ export const getElementInformation = async ( */ export const getSelectors = async (page: Page, coordinates: Coordinates) => { try { - const selectors : any = await page.evaluate(async ({ x, y }) => { - // version @medv/finder - // https://github.com/antonmedv/finder/blob/master/finder.ts - - type Node = { - name: string; - penalty: number; - level?: number; - }; - - type Path = Node[]; - - enum Limit { - All, - Two, - One, - } - - type Options = { - root: Element; - idName: (name: string) => boolean; - className: (name: string) => boolean; - tagName: (name: string) => boolean; - attr: (name: string, value: string) => boolean; - seedMinLength: number; - optimizedMinLength: number; - threshold: number; - maxNumberOfTries: number; - }; - - let config: Options; - - let rootDocument: Document | Element; - - function finder(input: Element, options?: Partial) { - if (input.nodeType !== Node.ELEMENT_NODE) { - throw new Error(`Can't generate CSS selector for non-element node type.`); - } - - if ('html' === input.tagName.toLowerCase()) { - return 'html'; - } - - const defaults: Options = { - root: document.body, - idName: (name: string) => true, - className: (name: string) => true, - tagName: (name: string) => true, - attr: (name: string, value: string) => false, - seedMinLength: 1, - optimizedMinLength: 2, - threshold: 1000, - maxNumberOfTries: 10000, - }; - - config = { ...defaults, ...options }; - - rootDocument = findRootDocument(config.root, defaults); - - let path = bottomUpSearch(input, Limit.All, () => - bottomUpSearch(input, Limit.Two, () => bottomUpSearch(input, Limit.One)) - ); - - if (path) { - const optimized = sort(optimize(path, input)); - - if (optimized.length > 0) { - path = optimized[0]; - } - - return selector(path); - } else { - throw new Error(`Selector was not found.`); - } - } - - function findRootDocument(rootNode: Element | Document, defaults: Options) { - if (rootNode.nodeType === Node.DOCUMENT_NODE) { - return rootNode; - } - if (rootNode === defaults.root) { - return rootNode.ownerDocument as Document; - } - return rootNode; - } - - function bottomUpSearch( - input: Element, - limit: Limit, - fallback?: () => Path | null - ): Path | null { - let path: Path | null = null; - let stack: Node[][] = []; - let current: Element | null = input; - let i = 0; - - while (current && current !== config.root.parentElement) { - let level: Node[] = maybe(id(current)) || - maybe(...attr(current)) || - maybe(...classNames(current)) || - maybe(tagName(current)) || [any()]; - - const nth = index(current); - - if (limit === Limit.All) { - if (nth) { - level = level.concat( - level.filter(dispensableNth).map((node) => nthChild(node, nth)) - ); - } - } else if (limit === Limit.Two) { - level = level.slice(0, 1); - - if (nth) { - level = level.concat( - level.filter(dispensableNth).map((node) => nthChild(node, nth)) - ); - } - } else if (limit === Limit.One) { - const [node] = (level = level.slice(0, 1)); - - if (nth && dispensableNth(node)) { - level = [nthChild(node, nth)]; - } - } - - for (let node of level) { - node.level = i; - } - - stack.push(level); - - if (stack.length >= config.seedMinLength) { - path = findUniquePath(stack, fallback); - if (path) { - break; - } - } - - current = current.parentElement; - i++; - } - - if (!path) { - path = findUniquePath(stack, fallback); - } - - return path; - } - - function findUniquePath( - stack: Node[][], - fallback?: () => Path | null - ): Path | null { - const paths = sort(combinations(stack)); - - if (paths.length > config.threshold) { - return fallback ? fallback() : null; - } - - for (let candidate of paths) { - if (unique(candidate)) { - return candidate; - } - } - - return null; - } - - function selector(path: Path): string { - let node = path[0]; - let query = node.name; - for (let i = 1; i < path.length; i++) { - const level = path[i].level || 0; - - if (node.level === level - 1) { - query = `${path[i].name} > ${query}`; - } else { - query = `${path[i].name} ${query}`; - } - - node = path[i]; - } - return query; - } - - function penalty(path: Path): number { - return path.map((node) => node.penalty).reduce((acc, i) => acc + i, 0); - } - - function unique(path: Path) { - switch (rootDocument.querySelectorAll(selector(path)).length) { - case 0: - throw new Error( - `Can't select any node with this selector: ${selector(path)}` - ); - case 1: - return true; - default: - return false; - } - } - - function id(input: Element): Node | null { - const elementId = input.getAttribute('id'); - if (elementId && config.idName(elementId)) { - return { - name: '#' + cssesc(elementId, { isIdentifier: true }), - penalty: 0, - }; - } - return null; - } - - function attr(input: Element): Node[] { - const attrs = Array.from(input.attributes).filter((attr) => - config.attr(attr.name, attr.value) - ); - - return attrs.map( - (attr): Node => ({ - name: - '[' + - cssesc(attr.name, { isIdentifier: true }) + - '="' + - cssesc(attr.value) + - '"]', - penalty: 0.5, - }) - ); - } - - function classNames(input: Element): Node[] { - const names = Array.from(input.classList).filter(config.className); - - return names.map( - (name): Node => ({ - name: '.' + cssesc(name, { isIdentifier: true }), - penalty: 1, - }) - ); - } - - function tagName(input: Element): Node | null { - const name = input.tagName.toLowerCase(); - if (config.tagName(name)) { - return { - name, - penalty: 2, - }; - } - return null; - } - - function any(): Node { - return { - name: '*', - penalty: 3, - }; - } - - function index(input: Element): number | null { - const parent = input.parentNode; - if (!parent) { - return null; - } - - let child = parent.firstChild; - if (!child) { - return null; - } - - let i = 0; - while (child) { - if (child.nodeType === Node.ELEMENT_NODE) { - i++; - } - - if (child === input) { - break; - } - - child = child.nextSibling; - } - - return i; - } - - function nthChild(node: Node, i: number): Node { - return { - name: node.name + `:nth-child(${i})`, - penalty: node.penalty + 1, - }; - } - - function dispensableNth(node: Node) { - return node.name !== 'html' && !node.name.startsWith('#'); - } - - function maybe(...level: (Node | null)[]): Node[] | null { - const list = level.filter(notEmpty); - if (list.length > 0) { - return list; - } - return null; - } - - function notEmpty(value: T | null | undefined): value is T { - return value !== null && value !== undefined; - } - - function* combinations(stack: Node[][], path: Node[] = []): Generator { - if (stack.length > 0) { - for (let node of stack[0]) { - yield* combinations(stack.slice(1, stack.length), path.concat(node)); - } - } else { - yield path; - } - } - - function sort(paths: Iterable): Path[] { - return Array.from(paths).sort((a, b) => penalty(a) - penalty(b)); - } - - type Scope = { - counter: number; - visited: Map; - }; - - function* optimize( - path: Path, - input: Element, - scope: Scope = { - counter: 0, - visited: new Map(), - } - ): Generator { - if (path.length > 2 && path.length > config.optimizedMinLength) { - for (let i = 1; i < path.length - 1; i++) { - if (scope.counter > config.maxNumberOfTries) { - return; // Okay At least I tried! - } - scope.counter += 1; - const newPath = [...path]; - newPath.splice(i, 1); - const newPathKey = selector(newPath); - if (scope.visited.has(newPathKey)) { - return; - } - if (unique(newPath) && same(newPath, input)) { - yield newPath; - scope.visited.set(newPathKey, true); - yield* optimize(newPath, input, scope); - } - } - } - } - - function same(path: Path, input: Element) { - return rootDocument.querySelector(selector(path)) === input; - } - - const regexAnySingleEscape = /[ -,\.\/:-@\[-\^`\{-~]/; - const regexSingleEscape = /[ -,\.\/:-@\[\]\^`\{-~]/; - const regexExcessiveSpaces = - /(^|\\+)?(\\[A-F0-9]{1,6})\x20(?![a-fA-F0-9\x20])/g; - - const defaultOptions = { - escapeEverything: false, - isIdentifier: false, - quotes: 'single', - wrap: false, - }; - - function cssesc(string: string, opt: Partial = {}) { - const options = { ...defaultOptions, ...opt }; - if (options.quotes != 'single' && options.quotes != 'double') { - options.quotes = 'single'; - } - const quote = options.quotes == 'double' ? '"' : "'"; - const isIdentifier = options.isIdentifier; - - const firstChar = string.charAt(0); - let output = ''; - let counter = 0; - const length = string.length; - while (counter < length) { - const character = string.charAt(counter++); - let codePoint = character.charCodeAt(0); - let value: string | undefined = void 0; - // If it’s not a printable ASCII character… - if (codePoint < 0x20 || codePoint > 0x7e) { - if (codePoint >= 0xd800 && codePoint <= 0xdbff && counter < length) { - // It’s a high surrogate, and there is a next character. - const extra = string.charCodeAt(counter++); - if ((extra & 0xfc00) == 0xdc00) { - // next character is low surrogate - codePoint = ((codePoint & 0x3ff) << 10) + (extra & 0x3ff) + 0x10000; - } else { - // It’s an unmatched surrogate; only append this code unit, in case - // the next code unit is the high surrogate of a surrogate pair. - counter--; - } - } - value = '\\' + codePoint.toString(16).toUpperCase() + ' '; - } else { - if (options.escapeEverything) { - if (regexAnySingleEscape.test(character)) { - value = '\\' + character; - } else { - value = '\\' + codePoint.toString(16).toUpperCase() + ' '; - } - } else if (/[\t\n\f\r\x0B]/.test(character)) { - value = '\\' + codePoint.toString(16).toUpperCase() + ' '; - } else if ( - character == '\\' || - (!isIdentifier && - ((character == '"' && quote == character) || - (character == "'" && quote == character))) || - (isIdentifier && regexSingleEscape.test(character)) - ) { - value = '\\' + character; - } else { - value = character; - } - } - output += value; - } - - if (isIdentifier) { - if (/^-[-\d]/.test(output)) { - output = '\\-' + output.slice(1); - } else if (/\d/.test(firstChar)) { - output = '\\3' + firstChar + ' ' + output.slice(1); - } - } - - // Remove spaces after `\HEX` escapes that are not followed by a hex digit, - // since they’re redundant. Note that this is only possible if the escape - // sequence isn’t preceded by an odd number of backslashes. - output = output.replace(regexExcessiveSpaces, function ($0, $1, $2) { - if ($1 && $1.length % 2) { - // It’s not safe to remove the space, so don’t. - return $0; - } - // Strip the space. - return ($1 || '') + $2; - }); - - if (!isIdentifier && options.wrap) { - return quote + output + quote; - } - return output; - } - - - - const genSelectors = (element: HTMLElement | null) =>{ - if (element == null) { - return null; - } - - const href = element.getAttribute('href'); - - let generalSelector = null; - try { - generalSelector = finder(element); - } catch (e) { - } - - let attrSelector = null; - try { - attrSelector = finder(element, { attr: () => true }); - } catch (e) { - } - - const hrefSelector = genSelectorForAttributes(element, ['href']); - const formSelector = genSelectorForAttributes(element, [ - 'name', - 'placeholder', - 'for', - ]); - const accessibilitySelector = genSelectorForAttributes(element, [ - 'aria-label', - 'alt', - 'title', - ]); - - const testIdSelector = genSelectorForAttributes(element, [ - 'data-testid', - 'data-test-id', - 'data-testing', - 'data-test', - 'data-qa', - 'data-cy', - ]); - - // We won't use an id selector if the id is invalid (starts with a number) - let idSelector = null; - try { - idSelector = - isAttributesDefined(element, ['id']) && - !isCharacterNumber(element.id?.[0]) - ? // Certain apps don't have unique ids (ex. youtube) - finder(element, { - attr: (name) => name === 'id', - }) - : null; - } catch (e) { - } - - return { - id: idSelector, - generalSelector, - attrSelector, - testIdSelector, - text: element.innerText, - href, - // Only try to pick an href selector if there is an href on the element - hrefSelector, - accessibilitySelector, - formSelector, - }; - } - - function genAttributeSet(element: HTMLElement, attributes: string[]) { - return new Set( - attributes.filter((attr) => { - const attrValue = element.getAttribute(attr); - return attrValue != null && attrValue.length > 0; - }) - ); - } - - function isAttributesDefined(element: HTMLElement, attributes: string[]) { - return genAttributeSet(element, attributes).size > 0; - } - -// Gets all attributes that aren't null and empty - function genValidAttributeFilter(element: HTMLElement, attributes: string[]) { - const attrSet = genAttributeSet(element, attributes); - - return (name: string) => attrSet.has(name); - } - - function genSelectorForAttributes(element: HTMLElement, attributes: string[]) { - let selector = null; - try { - selector = isAttributesDefined(element, attributes) - ? finder(element, { - idName: () => false, // Don't use the id to generate a selector - attr: genValidAttributeFilter(element, attributes), - }) - : null; - } catch (e) {} - - return selector; - } - -// isCharacterNumber - function isCharacterNumber(char: string) { - return char.length === 1 && char.match(/[0-9]/); - } - - const hoveredElement = document.elementFromPoint(x, y) as HTMLElement; - if ( - hoveredElement != null && - !hoveredElement.closest('#overlay-controls') != null - ) { - const { parentElement } = hoveredElement; - // Match the logic in recorder.ts for link clicks - const element = parentElement?.tagName === 'A' ? parentElement : hoveredElement; - const generatedSelectors = genSelectors(element); - return generatedSelectors; - } - }, { x: coordinates.x, y: coordinates.y }); - return selectors; - } catch (e) { - const { message, stack } = e as Error; - logger.log('error', `Error while retrieving element: ${message}`); - logger.log('error', `Stack: ${stack}`); - } - return null; + const selectors: any = await page.evaluate(async ({ x, y }) => { + // version @medv/finder + // https://github.com/antonmedv/finder/blob/master/finder.ts + + type Node = { + name: string; + penalty: number; + level?: number; + }; + + type Path = Node[]; + + enum Limit { + All, + Two, + One, + } + + type Options = { + root: Element; + idName: (name: string) => boolean; + className: (name: string) => boolean; + tagName: (name: string) => boolean; + attr: (name: string, value: string) => boolean; + seedMinLength: number; + optimizedMinLength: number; + threshold: number; + maxNumberOfTries: number; + }; + + let config: Options; + + let rootDocument: Document | Element; + + function finder(input: Element, options?: Partial) { + if (input.nodeType !== Node.ELEMENT_NODE) { + throw new Error(`Can't generate CSS selector for non-element node type.`); + } + + if ('html' === input.tagName.toLowerCase()) { + return 'html'; + } + + const defaults: Options = { + root: document.body, + idName: (name: string) => true, + className: (name: string) => true, + tagName: (name: string) => true, + attr: (name: string, value: string) => false, + seedMinLength: 1, + optimizedMinLength: 2, + threshold: 1000, + maxNumberOfTries: 10000, + }; + + config = { ...defaults, ...options }; + + rootDocument = findRootDocument(config.root, defaults); + + let path = bottomUpSearch(input, Limit.All, () => + bottomUpSearch(input, Limit.Two, () => bottomUpSearch(input, Limit.One)) + ); + + if (path) { + const optimized = sort(optimize(path, input)); + + if (optimized.length > 0) { + path = optimized[0]; + } + + return selector(path); + } else { + throw new Error(`Selector was not found.`); + } + } + + function findRootDocument(rootNode: Element | Document, defaults: Options) { + if (rootNode.nodeType === Node.DOCUMENT_NODE) { + return rootNode; + } + if (rootNode === defaults.root) { + return rootNode.ownerDocument as Document; + } + return rootNode; + } + + function bottomUpSearch( + input: Element, + limit: Limit, + fallback?: () => Path | null + ): Path | null { + let path: Path | null = null; + let stack: Node[][] = []; + let current: Element | null = input; + let i = 0; + + while (current && current !== config.root.parentElement) { + let level: Node[] = maybe(id(current)) || + maybe(...attr(current)) || + maybe(...classNames(current)) || + maybe(tagName(current)) || [any()]; + + const nth = index(current); + + if (limit === Limit.All) { + if (nth) { + level = level.concat( + level.filter(dispensableNth).map((node) => nthChild(node, nth)) + ); + } + } else if (limit === Limit.Two) { + level = level.slice(0, 1); + + if (nth) { + level = level.concat( + level.filter(dispensableNth).map((node) => nthChild(node, nth)) + ); + } + } else if (limit === Limit.One) { + const [node] = (level = level.slice(0, 1)); + + if (nth && dispensableNth(node)) { + level = [nthChild(node, nth)]; + } + } + + for (let node of level) { + node.level = i; + } + + stack.push(level); + + if (stack.length >= config.seedMinLength) { + path = findUniquePath(stack, fallback); + if (path) { + break; + } + } + + current = current.parentElement; + i++; + } + + if (!path) { + path = findUniquePath(stack, fallback); + } + + return path; + } + + function findUniquePath( + stack: Node[][], + fallback?: () => Path | null + ): Path | null { + const paths = sort(combinations(stack)); + + if (paths.length > config.threshold) { + return fallback ? fallback() : null; + } + + for (let candidate of paths) { + if (unique(candidate)) { + return candidate; + } + } + + return null; + } + + function selector(path: Path): string { + let node = path[0]; + let query = node.name; + for (let i = 1; i < path.length; i++) { + const level = path[i].level || 0; + + if (node.level === level - 1) { + query = `${path[i].name} > ${query}`; + } else { + query = `${path[i].name} ${query}`; + } + + node = path[i]; + } + return query; + } + + function penalty(path: Path): number { + return path.map((node) => node.penalty).reduce((acc, i) => acc + i, 0); + } + + function unique(path: Path) { + switch (rootDocument.querySelectorAll(selector(path)).length) { + case 0: + throw new Error( + `Can't select any node with this selector: ${selector(path)}` + ); + case 1: + return true; + default: + return false; + } + } + + function id(input: Element): Node | null { + const elementId = input.getAttribute('id'); + if (elementId && config.idName(elementId)) { + return { + name: '#' + cssesc(elementId, { isIdentifier: true }), + penalty: 0, + }; + } + return null; + } + + function attr(input: Element): Node[] { + const attrs = Array.from(input.attributes).filter((attr) => + config.attr(attr.name, attr.value) + ); + + return attrs.map( + (attr): Node => ({ + name: + '[' + + cssesc(attr.name, { isIdentifier: true }) + + '="' + + cssesc(attr.value) + + '"]', + penalty: 0.5, + }) + ); + } + + function classNames(input: Element): Node[] { + const names = Array.from(input.classList).filter(config.className); + + return names.map( + (name): Node => ({ + name: '.' + cssesc(name, { isIdentifier: true }), + penalty: 1, + }) + ); + } + + function tagName(input: Element): Node | null { + const name = input.tagName.toLowerCase(); + if (config.tagName(name)) { + return { + name, + penalty: 2, + }; + } + return null; + } + + function any(): Node { + return { + name: '*', + penalty: 3, + }; + } + + function index(input: Element): number | null { + const parent = input.parentNode; + if (!parent) { + return null; + } + + let child = parent.firstChild; + if (!child) { + return null; + } + + let i = 0; + while (child) { + if (child.nodeType === Node.ELEMENT_NODE) { + i++; + } + + if (child === input) { + break; + } + + child = child.nextSibling; + } + + return i; + } + + function nthChild(node: Node, i: number): Node { + return { + name: node.name + `:nth-child(${i})`, + penalty: node.penalty + 1, + }; + } + + function dispensableNth(node: Node) { + return node.name !== 'html' && !node.name.startsWith('#'); + } + + function maybe(...level: (Node | null)[]): Node[] | null { + const list = level.filter(notEmpty); + if (list.length > 0) { + return list; + } + return null; + } + + function notEmpty(value: T | null | undefined): value is T { + return value !== null && value !== undefined; + } + + function* combinations(stack: Node[][], path: Node[] = []): Generator { + if (stack.length > 0) { + for (let node of stack[0]) { + yield* combinations(stack.slice(1, stack.length), path.concat(node)); + } + } else { + yield path; + } + } + + function sort(paths: Iterable): Path[] { + return Array.from(paths).sort((a, b) => penalty(a) - penalty(b)); + } + + type Scope = { + counter: number; + visited: Map; + }; + + function* optimize( + path: Path, + input: Element, + scope: Scope = { + counter: 0, + visited: new Map(), + } + ): Generator { + if (path.length > 2 && path.length > config.optimizedMinLength) { + for (let i = 1; i < path.length - 1; i++) { + if (scope.counter > config.maxNumberOfTries) { + return; // Okay At least I tried! + } + scope.counter += 1; + const newPath = [...path]; + newPath.splice(i, 1); + const newPathKey = selector(newPath); + if (scope.visited.has(newPathKey)) { + return; + } + if (unique(newPath) && same(newPath, input)) { + yield newPath; + scope.visited.set(newPathKey, true); + yield* optimize(newPath, input, scope); + } + } + } + } + + function same(path: Path, input: Element) { + return rootDocument.querySelector(selector(path)) === input; + } + + const regexAnySingleEscape = /[ -,\.\/:-@\[-\^`\{-~]/; + const regexSingleEscape = /[ -,\.\/:-@\[\]\^`\{-~]/; + const regexExcessiveSpaces = + /(^|\\+)?(\\[A-F0-9]{1,6})\x20(?![a-fA-F0-9\x20])/g; + + const defaultOptions = { + escapeEverything: false, + isIdentifier: false, + quotes: 'single', + wrap: false, + }; + + function cssesc(string: string, opt: Partial = {}) { + const options = { ...defaultOptions, ...opt }; + if (options.quotes != 'single' && options.quotes != 'double') { + options.quotes = 'single'; + } + const quote = options.quotes == 'double' ? '"' : "'"; + const isIdentifier = options.isIdentifier; + + const firstChar = string.charAt(0); + let output = ''; + let counter = 0; + const length = string.length; + while (counter < length) { + const character = string.charAt(counter++); + let codePoint = character.charCodeAt(0); + let value: string | undefined = void 0; + // If it’s not a printable ASCII character… + if (codePoint < 0x20 || codePoint > 0x7e) { + if (codePoint >= 0xd800 && codePoint <= 0xdbff && counter < length) { + // It’s a high surrogate, and there is a next character. + const extra = string.charCodeAt(counter++); + if ((extra & 0xfc00) == 0xdc00) { + // next character is low surrogate + codePoint = ((codePoint & 0x3ff) << 10) + (extra & 0x3ff) + 0x10000; + } else { + // It’s an unmatched surrogate; only append this code unit, in case + // the next code unit is the high surrogate of a surrogate pair. + counter--; + } + } + value = '\\' + codePoint.toString(16).toUpperCase() + ' '; + } else { + if (options.escapeEverything) { + if (regexAnySingleEscape.test(character)) { + value = '\\' + character; + } else { + value = '\\' + codePoint.toString(16).toUpperCase() + ' '; + } + } else if (/[\t\n\f\r\x0B]/.test(character)) { + value = '\\' + codePoint.toString(16).toUpperCase() + ' '; + } else if ( + character == '\\' || + (!isIdentifier && + ((character == '"' && quote == character) || + (character == "'" && quote == character))) || + (isIdentifier && regexSingleEscape.test(character)) + ) { + value = '\\' + character; + } else { + value = character; + } + } + output += value; + } + + if (isIdentifier) { + if (/^-[-\d]/.test(output)) { + output = '\\-' + output.slice(1); + } else if (/\d/.test(firstChar)) { + output = '\\3' + firstChar + ' ' + output.slice(1); + } + } + + // Remove spaces after `\HEX` escapes that are not followed by a hex digit, + // since they’re redundant. Note that this is only possible if the escape + // sequence isn’t preceded by an odd number of backslashes. + output = output.replace(regexExcessiveSpaces, function ($0, $1, $2) { + if ($1 && $1.length % 2) { + // It’s not safe to remove the space, so don’t. + return $0; + } + // Strip the space. + return ($1 || '') + $2; + }); + + if (!isIdentifier && options.wrap) { + return quote + output + quote; + } + return output; + } + + + + const genSelectors = (element: HTMLElement | null) => { + if (element == null) { + return null; + } + + const href = element.getAttribute('href'); + + let generalSelector = null; + try { + generalSelector = finder(element); + } catch (e) { + } + + let attrSelector = null; + try { + attrSelector = finder(element, { attr: () => true }); + } catch (e) { + } + + const hrefSelector = genSelectorForAttributes(element, ['href']); + const formSelector = genSelectorForAttributes(element, [ + 'name', + 'placeholder', + 'for', + ]); + const accessibilitySelector = genSelectorForAttributes(element, [ + 'aria-label', + 'alt', + 'title', + ]); + + const testIdSelector = genSelectorForAttributes(element, [ + 'data-testid', + 'data-test-id', + 'data-testing', + 'data-test', + 'data-qa', + 'data-cy', + ]); + + // We won't use an id selector if the id is invalid (starts with a number) + let idSelector = null; + try { + idSelector = + isAttributesDefined(element, ['id']) && + !isCharacterNumber(element.id?.[0]) + ? // Certain apps don't have unique ids (ex. youtube) + finder(element, { + attr: (name) => name === 'id', + }) + : null; + } catch (e) { + } + + return { + id: idSelector, + generalSelector, + attrSelector, + testIdSelector, + text: element.innerText, + href, + // Only try to pick an href selector if there is an href on the element + hrefSelector, + accessibilitySelector, + formSelector, + }; + } + + function genAttributeSet(element: HTMLElement, attributes: string[]) { + return new Set( + attributes.filter((attr) => { + const attrValue = element.getAttribute(attr); + return attrValue != null && attrValue.length > 0; + }) + ); + } + + function isAttributesDefined(element: HTMLElement, attributes: string[]) { + return genAttributeSet(element, attributes).size > 0; + } + + // Gets all attributes that aren't null and empty + function genValidAttributeFilter(element: HTMLElement, attributes: string[]) { + const attrSet = genAttributeSet(element, attributes); + + return (name: string) => attrSet.has(name); + } + + function genSelectorForAttributes(element: HTMLElement, attributes: string[]) { + let selector = null; + try { + selector = isAttributesDefined(element, attributes) + ? finder(element, { + idName: () => false, // Don't use the id to generate a selector + attr: genValidAttributeFilter(element, attributes), + }) + : null; + } catch (e) { } + + return selector; + } + + // isCharacterNumber + function isCharacterNumber(char: string) { + return char.length === 1 && char.match(/[0-9]/); + } + + const hoveredElement = document.elementFromPoint(x, y) as HTMLElement; + if ( + hoveredElement != null && + !hoveredElement.closest('#overlay-controls') != null + ) { + const { parentElement } = hoveredElement; + // Match the logic in recorder.ts for link clicks + const element = parentElement?.tagName === 'A' ? parentElement : hoveredElement; + const generatedSelectors = genSelectors(element); + return generatedSelectors; + } + }, { x: coordinates.x, y: coordinates.y }); + return selectors; + } catch (e) { + const { message, stack } = e as Error; + logger.log('error', `Error while retrieving element: ${message}`); + logger.log('error', `Stack: ${stack}`); + } + return null; }; /** @@ -711,11 +712,11 @@ export const selectorAlreadyInWorkflow = (selector: string, workflow: Workflow) * @param page The page to use for the validation. * @category WorkflowManagement */ -export const isRuleOvershadowing = async(selectors: string[], page:Page): Promise => { - for (const selector of selectors){ +export const isRuleOvershadowing = async (selectors: string[], page: Page): Promise => { + for (const selector of selectors) { const areElsVisible = await page.$$eval(selector, (elems) => { - const isVisible = ( elem: HTMLElement | SVGElement ) => { + const isVisible = (elem: HTMLElement | SVGElement) => { if (elem instanceof HTMLElement) { return !!(elem.offsetWidth || elem.offsetHeight @@ -735,7 +736,7 @@ export const isRuleOvershadowing = async(selectors: string[], page:Page): Promis return false } - if (areElsVisible.includes(false)){ + if (areElsVisible.includes(false)) { return false; } } From 88b2fe4c2ead4d48f4163823d0dfbab07ccc8ee1 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Thu, 6 Jun 2024 05:14:33 +0530 Subject: [PATCH 76/83] chore: remove whitespace --- server/src/workflow-management/selector.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index 47f348b0..957cb3b9 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -741,8 +741,4 @@ export const isRuleOvershadowing = async (selectors: string[], page: Page): Prom } } return true; -} - - - - +} \ No newline at end of file From 343c34ca3b753d092896f03f8b891528c04f5c4d Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Thu, 6 Jun 2024 05:42:23 +0530 Subject: [PATCH 77/83] feat: click, hover, dnd actions --- server/src/workflow-management/utils.ts | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 server/src/workflow-management/utils.ts diff --git a/server/src/workflow-management/utils.ts b/server/src/workflow-management/utils.ts new file mode 100644 index 00000000..3f5cef9b --- /dev/null +++ b/server/src/workflow-management/utils.ts @@ -0,0 +1,24 @@ +import { Action, ActionType, TagName } from "../types"; + +/** + * A helper function to get the best selector for the specific user action. + * @param action The user action. + * @returns {string|null} + * @category WorkflowManagement-Selectors + */ +export const getBestSelectorForAction = (action: Action) => { + switch (action.type) { + case ActionType.Click: + case ActionType.Hover: + case ActionType.DragAndDrop: { + const selectors = action.selectors; + // less than 25 characters, and element only has text inside + const textSelector = + selectors?.text?.length != null && + selectors?.text?.length < 25 && + action.hasOnlyText + ? `text=${selectors.text}` + : null; + + } +} From 5940dd1d3defa3692e0b67832b02b6cb8aeedbf0 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Thu, 6 Jun 2024 05:43:16 +0530 Subject: [PATCH 78/83] feat: tagname input selectors --- server/src/workflow-management/utils.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/server/src/workflow-management/utils.ts b/server/src/workflow-management/utils.ts index 3f5cef9b..42e8887f 100644 --- a/server/src/workflow-management/utils.ts +++ b/server/src/workflow-management/utils.ts @@ -20,5 +20,17 @@ export const getBestSelectorForAction = (action: Action) => { ? `text=${selectors.text}` : null; - } + if (action.tagName === TagName.Input) { + return ( + selectors.testIdSelector ?? + selectors?.id ?? + selectors?.formSelector ?? + selectors?.accessibilitySelector ?? + selectors?.generalSelector ?? + selectors?.attrSelector ?? + null + ); + } + + } From 4c8312fe765e7f4b02c139cd5461ffcf267d2673 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Thu, 6 Jun 2024 05:43:43 +0530 Subject: [PATCH 79/83] feat: tagname A selectors --- server/src/workflow-management/utils.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/server/src/workflow-management/utils.ts b/server/src/workflow-management/utils.ts index 42e8887f..3574c56a 100644 --- a/server/src/workflow-management/utils.ts +++ b/server/src/workflow-management/utils.ts @@ -31,6 +31,17 @@ export const getBestSelectorForAction = (action: Action) => { null ); } - - + if (action.tagName === TagName.A) { + return ( + selectors.testIdSelector ?? + selectors?.id ?? + selectors?.hrefSelector ?? + selectors?.accessibilitySelector ?? + selectors?.generalSelector ?? + selectors?.attrSelector ?? + null + ); + } + + } From 8da75c5419e3392ee2e1c4a6bbf4ae3745b33070 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Thu, 6 Jun 2024 05:44:41 +0530 Subject: [PATCH 80/83] feat: span, em, cite, B, strong selectors --- server/src/workflow-management/utils.ts | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/server/src/workflow-management/utils.ts b/server/src/workflow-management/utils.ts index 3574c56a..43783d97 100644 --- a/server/src/workflow-management/utils.ts +++ b/server/src/workflow-management/utils.ts @@ -43,5 +43,24 @@ export const getBestSelectorForAction = (action: Action) => { ); } - + + if ( + action.tagName === TagName.Span || + action.tagName === TagName.EM || + action.tagName === TagName.Cite || + action.tagName === TagName.B || + action.tagName === TagName.Strong + ) { + return ( + selectors.testIdSelector ?? + selectors?.id ?? + selectors?.accessibilitySelector ?? + selectors?.hrefSelector ?? + textSelector ?? + selectors?.generalSelector ?? + selectors?.attrSelector ?? + null + ); + } + } From 0fd592771513dba12e30e6036f3e51c53f469ae9 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Thu, 6 Jun 2024 05:44:55 +0530 Subject: [PATCH 81/83] chore: comment --- server/src/workflow-management/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/workflow-management/utils.ts b/server/src/workflow-management/utils.ts index 43783d97..2523ff1b 100644 --- a/server/src/workflow-management/utils.ts +++ b/server/src/workflow-management/utils.ts @@ -43,7 +43,7 @@ export const getBestSelectorForAction = (action: Action) => { ); } - + // Prefer text selectors for spans, ems over general selectors if ( action.tagName === TagName.Span || action.tagName === TagName.EM || From 8f33ae6b60cb1216cadb6bf628275c6c350260ab Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Thu, 6 Jun 2024 05:45:19 +0530 Subject: [PATCH 82/83] feat: input & keydown actions --- server/src/workflow-management/utils.ts | 28 ++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/server/src/workflow-management/utils.ts b/server/src/workflow-management/utils.ts index 2523ff1b..4cf1232f 100644 --- a/server/src/workflow-management/utils.ts +++ b/server/src/workflow-management/utils.ts @@ -62,5 +62,31 @@ export const getBestSelectorForAction = (action: Action) => { null ); } - + return ( + selectors.testIdSelector ?? + selectors?.id ?? + selectors?.accessibilitySelector ?? + selectors?.hrefSelector ?? + selectors?.generalSelector ?? + selectors?.attrSelector ?? + null + ); + } + case ActionType.Input: + case ActionType.Keydown: { + const selectors = action.selectors; + return ( + selectors.testIdSelector ?? + selectors?.id ?? + selectors?.formSelector ?? + selectors?.accessibilitySelector ?? + selectors?.generalSelector ?? + selectors?.attrSelector ?? + null + ); + } + default: + break; + } + return null; } From 8ce339f1767c486222d5fc9253e8208dfab6752c Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Thu, 6 Jun 2024 05:45:38 +0530 Subject: [PATCH 83/83] chore: lint --- server/src/workflow-management/utils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/workflow-management/utils.ts b/server/src/workflow-management/utils.ts index 4cf1232f..775a0b55 100644 --- a/server/src/workflow-management/utils.ts +++ b/server/src/workflow-management/utils.ts @@ -15,8 +15,8 @@ export const getBestSelectorForAction = (action: Action) => { // less than 25 characters, and element only has text inside const textSelector = selectors?.text?.length != null && - selectors?.text?.length < 25 && - action.hasOnlyText + selectors?.text?.length < 25 && + action.hasOnlyText ? `text=${selectors.text}` : null;