From c994072ef70b25d516a0b9d59f832949418ab6c3 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Wed, 11 Dec 2024 02:00:43 +0530 Subject: [PATCH 01/88] feat: pass listSelector getRect and getElementInfo --- server/src/workflow-management/classes/Generator.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/server/src/workflow-management/classes/Generator.ts b/server/src/workflow-management/classes/Generator.ts index c9dc3385..1e81cd4a 100644 --- a/server/src/workflow-management/classes/Generator.ts +++ b/server/src/workflow-management/classes/Generator.ts @@ -541,7 +541,9 @@ export class WorkflowGenerator { * @returns {Promise} */ private generateSelector = async (page: Page, coordinates: Coordinates, action: ActionType) => { - const elementInfo = await getElementInformation(page, coordinates); + const elementInfo = await getElementInformation(page, coordinates, this.listSelector); + + console.log(`List selector: ${this.listSelector}`) const selectorBasedOnCustomAction = (this.getList === true) ? await getNonUniqueSelectors(page, coordinates) @@ -570,9 +572,9 @@ export class WorkflowGenerator { * @returns {Promise} */ public generateDataForHighlighter = async (page: Page, coordinates: Coordinates) => { - const rect = await getRect(page, coordinates); + const rect = await getRect(page, coordinates, this.listSelector); const displaySelector = await this.generateSelector(page, coordinates, ActionType.Click); - const elementInfo = await getElementInformation(page, coordinates); + const elementInfo = await getElementInformation(page, coordinates, this.listSelector); if (rect) { if (this.getList === true) { if (this.listSelector !== '') { From 81bbba473fce2e37f031329305e9647c637940a6 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Wed, 11 Dec 2024 02:01:55 +0530 Subject: [PATCH 02/88] feat: condtionally handle getRect & getelementInfo --- server/src/workflow-management/selector.ts | 366 ++++++++++++--------- 1 file changed, 217 insertions(+), 149 deletions(-) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index 917ac561..b383d653 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -22,100 +22,144 @@ type Workflow = WorkflowFile["workflow"]; */ export const getElementInformation = async ( page: Page, - coordinates: Coordinates + coordinates: Coordinates, + listSelector: string, ) => { try { - const elementInfo = await page.evaluate( - async ({ x, y }) => { - const originalEl = document.elementFromPoint(x, y) as HTMLElement; - if (originalEl) { - let element = originalEl; - - // if (originalEl.tagName === 'A') { - // element = originalEl; - // } else if (originalEl.parentElement?.tagName === 'A') { - // element = originalEl.parentElement; - // } else { - // Generic parent finding logic based on visual containment - const containerTags = ['DIV', 'SECTION', 'ARTICLE', 'MAIN', 'HEADER', 'FOOTER', 'NAV', 'ASIDE', - 'ADDRESS', 'BLOCKQUOTE', 'DETAILS', 'DIALOG', 'FIGURE', 'FIGCAPTION', 'MAIN', 'MARK', 'SUMMARY', 'TIME', - 'TABLE', 'THEAD', 'TBODY', 'TFOOT', 'TR', 'TH', 'TD', 'CAPTION', 'COLGROUP', 'COL', 'FORM', 'FIELDSET', - 'LEGEND', 'LABEL', 'INPUT', 'BUTTON', 'SELECT', 'DATALIST', 'OPTGROUP', 'OPTION', 'TEXTAREA', 'OUTPUT', - 'PROGRESS', 'METER', 'DETAILS', 'SUMMARY', 'MENU', 'MENUITEM', 'MENUITEM', 'APPLET', 'EMBED', 'OBJECT', - 'PARAM', 'VIDEO', 'AUDIO', 'SOURCE', 'TRACK', 'CANVAS', 'MAP', 'AREA', 'SVG', 'IFRAME', 'FRAME', 'FRAMESET', - 'LI', 'UL', 'OL', 'DL', 'DT', 'DD', 'HR', 'P', 'PRE', 'LISTING', 'PLAINTEXT', 'A' - ]; - while (element.parentElement) { - const parentRect = element.parentElement.getBoundingClientRect(); - const childRect = element.getBoundingClientRect(); + if (listSelector !== '') { + // Old implementation + const elementInfo = await page.evaluate( + async ({ x, y }) => { + const el = document.elementFromPoint(x, y) as HTMLElement; + if (el) { + const { parentElement } = el; + const element = parentElement?.tagName === 'A' ? parentElement : el; + let info: { + tagName: string; + hasOnlyText?: boolean; + innerText?: string; + url?: string; + imageUrl?: string; + attributes?: Record; + innerHTML?: string; + outerHTML?: string; + } = { + tagName: element?.tagName ?? '', + }; + if (element) { + info.attributes = Array.from(element.attributes).reduce( + (acc, attr) => { + acc[attr.name] = attr.value; + return acc; + }, + {} as Record + ); + } + // Gather specific information based on the tag + if (element?.tagName === 'A') { + info.url = (element as HTMLAnchorElement).href; + info.innerText = element.innerText ?? ''; + } else if (element?.tagName === 'IMG') { + info.imageUrl = (element as HTMLImageElement).src; + } else { + info.hasOnlyText = element?.children?.length === 0 && + element?.innerText?.length > 0; + info.innerText = element?.innerText ?? ''; + } + info.innerHTML = element.innerHTML; + info.outerHTML = element.outerHTML; + return info; + } + return null; + }, + { x: coordinates.x, y: coordinates.y }, + ); + return elementInfo; + } else { + // New implementation + const elementInfo = await page.evaluate( + async ({ x, y }) => { + const originalEl = document.elementFromPoint(x, y) as HTMLElement; + if (originalEl) { + let element = originalEl; + + const containerTags = ['DIV', 'SECTION', 'ARTICLE', 'MAIN', 'HEADER', 'FOOTER', 'NAV', 'ASIDE', + 'ADDRESS', 'BLOCKQUOTE', 'DETAILS', 'DIALOG', 'FIGURE', 'FIGCAPTION', 'MAIN', 'MARK', 'SUMMARY', 'TIME', + 'TABLE', 'THEAD', 'TBODY', 'TFOOT', 'TR', 'TH', 'TD', 'CAPTION', 'COLGROUP', 'COL', 'FORM', 'FIELDSET', + 'LEGEND', 'LABEL', 'INPUT', 'BUTTON', 'SELECT', 'DATALIST', 'OPTGROUP', 'OPTION', 'TEXTAREA', 'OUTPUT', + 'PROGRESS', 'METER', 'DETAILS', 'SUMMARY', 'MENU', 'MENUITEM', 'MENUITEM', 'APPLET', 'EMBED', 'OBJECT', + 'PARAM', 'VIDEO', 'AUDIO', 'SOURCE', 'TRACK', 'CANVAS', 'MAP', 'AREA', 'SVG', 'IFRAME', 'FRAME', 'FRAMESET', + 'LI', 'UL', 'OL', 'DL', 'DT', 'DD', 'HR', 'P', 'PRE', 'LISTING', 'PLAINTEXT' + ]; + while (element.parentElement) { + const parentRect = element.parentElement.getBoundingClientRect(); + const childRect = element.getBoundingClientRect(); - if (!containerTags.includes(element.parentElement.tagName)) { - break; + if (!containerTags.includes(element.parentElement.tagName)) { + break; + } + + const fullyContained = + parentRect.left <= childRect.left && + parentRect.right >= childRect.right && + parentRect.top <= childRect.top && + parentRect.bottom >= childRect.bottom; + + const significantOverlap = + (childRect.width * childRect.height) / + (parentRect.width * parentRect.height) > 0.5; + + if (fullyContained && significantOverlap) { + element = element.parentElement; + } else { + break; + } } - // Check if parent visually contains the child - const fullyContained = - parentRect.left <= childRect.left && - parentRect.right >= childRect.right && - parentRect.top <= childRect.top && - parentRect.bottom >= childRect.bottom; + let info: { + tagName: string; + hasOnlyText?: boolean; + innerText?: string; + url?: string; + imageUrl?: string; + attributes?: Record; + innerHTML?: string; + outerHTML?: string; + } = { + tagName: element?.tagName ?? '', + }; - // Additional checks for more comprehensive containment - const significantOverlap = - (childRect.width * childRect.height) / - (parentRect.width * parentRect.height) > 0.5; + if (element) { + info.attributes = Array.from(element.attributes).reduce( + (acc, attr) => { + acc[attr.name] = attr.value; + return acc; + }, + {} as Record + ); + } - if (fullyContained && significantOverlap) { - element = element.parentElement; + if (element?.tagName === 'A') { + info.url = (element as HTMLAnchorElement).href; + info.innerText = element.innerText ?? ''; + } else if (element?.tagName === 'IMG') { + info.imageUrl = (element as HTMLImageElement).src; } else { - break; - // } - } } + info.hasOnlyText = element?.children?.length === 0 && + element?.innerText?.length > 0; + info.innerText = element?.innerText ?? ''; + } - let info: { - tagName: string; - hasOnlyText?: boolean; - innerText?: string; - url?: string; - imageUrl?: string; - attributes?: Record; - innerHTML?: string; - outerHTML?: string; - } = { - tagName: element?.tagName ?? '', - }; - - if (element) { - info.attributes = Array.from(element.attributes).reduce( - (acc, attr) => { - acc[attr.name] = attr.value; - return acc; - }, - {} as Record - ); + info.innerHTML = element.innerHTML; + info.outerHTML = element.outerHTML; + return info; } - - // Existing tag-specific logic - if (element?.tagName === 'A') { - info.url = (element as HTMLAnchorElement).href; - info.innerText = element.innerText ?? ''; - } else if (element?.tagName === 'IMG') { - info.imageUrl = (element as HTMLImageElement).src; - } else { - info.hasOnlyText = element?.children?.length === 0 && - element?.innerText?.length > 0; - info.innerText = element?.innerText ?? ''; - } - - info.innerHTML = element.innerHTML; - info.outerHTML = element.outerHTML; - return info; - } - return null; - }, - { x: coordinates.x, y: coordinates.y }, - ); - return elementInfo; + return null; + }, + { x: coordinates.x, y: coordinates.y }, + ); + return elementInfo; + } } catch (error) { const { message, stack } = error as Error; console.error('Error while retrieving selector:', message); @@ -123,79 +167,103 @@ export const getElementInformation = async ( } }; -export const getRect = async (page: Page, coordinates: Coordinates) => { +export const getRect = async (page: Page, coordinates: Coordinates, listSelector: string) => { try { - const rect = await page.evaluate( - async ({ x, y }) => { - const originalEl = document.elementFromPoint(x, y) as HTMLElement; - if (originalEl) { - let element = originalEl; - - // if (originalEl.tagName === 'A') { - // element = originalEl; - // } else if (originalEl.parentElement?.tagName === 'A') { - // element = originalEl.parentElement; - // } else { - const containerTags = ['DIV', 'SECTION', 'ARTICLE', 'MAIN', 'HEADER', 'FOOTER', 'NAV', 'ASIDE', - 'ADDRESS', 'BLOCKQUOTE', 'DETAILS', 'DIALOG', 'FIGURE', 'FIGCAPTION', 'MAIN', 'MARK', 'SUMMARY', 'TIME', - 'TABLE', 'THEAD', 'TBODY', 'TFOOT', 'TR', 'TH', 'TD', 'CAPTION', 'COLGROUP', 'COL', 'FORM', 'FIELDSET', - 'LEGEND', 'LABEL', 'INPUT', 'BUTTON', 'SELECT', 'DATALIST', 'OPTGROUP', 'OPTION', 'TEXTAREA', 'OUTPUT', - 'PROGRESS', 'METER', 'DETAILS', 'SUMMARY', 'MENU', 'MENUITEM', 'MENUITEM', 'APPLET', 'EMBED', 'OBJECT', - 'PARAM', 'VIDEO', 'AUDIO', 'SOURCE', 'TRACK', 'CANVAS', 'MAP', 'AREA', 'SVG', 'IFRAME', 'FRAME', 'FRAMESET', - 'LI', 'UL', 'OL', 'DL', 'DT', 'DD', 'HR', 'P', 'PRE', 'LISTING', 'PLAINTEXT', 'A' - ]; - while (element.parentElement) { - const parentRect = element.parentElement.getBoundingClientRect(); - const childRect = element.getBoundingClientRect(); - - if (!containerTags.includes(element.parentElement.tagName)) { - break; + if (listSelector !== '') { + // Old implementation + const rect = 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; + const rectangle = element?.getBoundingClientRect(); + 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 fullyContained = - parentRect.left <= childRect.left && - parentRect.right >= childRect.right && - parentRect.top <= childRect.top && - parentRect.bottom >= childRect.bottom; - - const significantOverlap = - (childRect.width * childRect.height) / - (parentRect.width * parentRect.height) > 0.5; - - if (fullyContained && significantOverlap) { - element = element.parentElement; - } else { - break; - // } - }} - - //element = element?.parentElement?.tagName === 'A' ? element?.parentElement : element; - const rectangle = element?.getBoundingClientRect(); - - 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; + }, + { x: coordinates.x, y: coordinates.y }, + ); + return rect; + } else { + // New implementation + const rect = await page.evaluate( + async ({ x, y }) => { + const originalEl = document.elementFromPoint(x, y) as HTMLElement; + if (originalEl) { + let element = originalEl; + + const containerTags = ['DIV', 'SECTION', 'ARTICLE', 'MAIN', 'HEADER', 'FOOTER', 'NAV', 'ASIDE', + 'ADDRESS', 'BLOCKQUOTE', 'DETAILS', 'DIALOG', 'FIGURE', 'FIGCAPTION', 'MAIN', 'MARK', 'SUMMARY', 'TIME', + 'TABLE', 'THEAD', 'TBODY', 'TFOOT', 'TR', 'TH', 'TD', 'CAPTION', 'COLGROUP', 'COL', 'FORM', 'FIELDSET', + 'LEGEND', 'LABEL', 'INPUT', 'BUTTON', 'SELECT', 'DATALIST', 'OPTGROUP', 'OPTION', 'TEXTAREA', 'OUTPUT', + 'PROGRESS', 'METER', 'DETAILS', 'SUMMARY', 'MENU', 'MENUITEM', 'MENUITEM', 'APPLET', 'EMBED', 'OBJECT', + 'PARAM', 'VIDEO', 'AUDIO', 'SOURCE', 'TRACK', 'CANVAS', 'MAP', 'AREA', 'SVG', 'IFRAME', 'FRAME', 'FRAMESET', + 'LI', 'UL', 'OL', 'DL', 'DT', 'DD', 'HR', 'P', 'PRE', 'LISTING', 'PLAINTEXT' + ]; + while (element.parentElement) { + const parentRect = element.parentElement.getBoundingClientRect(); + const childRect = element.getBoundingClientRect(); + + if (!containerTags.includes(element.parentElement.tagName)) { + break; + } + + const fullyContained = + parentRect.left <= childRect.left && + parentRect.right >= childRect.right && + parentRect.top <= childRect.top && + parentRect.bottom >= childRect.bottom; + + const significantOverlap = + (childRect.width * childRect.height) / + (parentRect.width * parentRect.height) > 0.5; + + if (fullyContained && significantOverlap) { + element = element.parentElement; + } else { + break; + } + } + + const rectangle = element?.getBoundingClientRect(); + + 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, + }; + } + } + return null; + }, + { x: coordinates.x, y: coordinates.y }, + ); + return rect; + } } catch (error) { const { message, stack } = error as Error; logger.log('error', `Error while retrieving selector: ${message}`); logger.log('error', `Stack: ${stack}`); } -} +}; /** From a34e8657735de66fdab71b08346fd1695acd1b21 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Wed, 11 Dec 2024 02:02:09 +0530 Subject: [PATCH 03/88] chore: lint --- server/src/workflow-management/selector.ts | 34 +++++++++++----------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index b383d653..66186a80 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -82,8 +82,8 @@ export const getElementInformation = async ( const originalEl = document.elementFromPoint(x, y) as HTMLElement; if (originalEl) { let element = originalEl; - - const containerTags = ['DIV', 'SECTION', 'ARTICLE', 'MAIN', 'HEADER', 'FOOTER', 'NAV', 'ASIDE', + + const containerTags = ['DIV', 'SECTION', 'ARTICLE', 'MAIN', 'HEADER', 'FOOTER', 'NAV', 'ASIDE', 'ADDRESS', 'BLOCKQUOTE', 'DETAILS', 'DIALOG', 'FIGURE', 'FIGCAPTION', 'MAIN', 'MARK', 'SUMMARY', 'TIME', 'TABLE', 'THEAD', 'TBODY', 'TFOOT', 'TR', 'TH', 'TD', 'CAPTION', 'COLGROUP', 'COL', 'FORM', 'FIELDSET', 'LEGEND', 'LABEL', 'INPUT', 'BUTTON', 'SELECT', 'DATALIST', 'OPTGROUP', 'OPTION', 'TEXTAREA', 'OUTPUT', @@ -99,14 +99,14 @@ export const getElementInformation = async ( break; } - const fullyContained = + const fullyContained = parentRect.left <= childRect.left && parentRect.right >= childRect.right && parentRect.top <= childRect.top && parentRect.bottom >= childRect.bottom; - const significantOverlap = - (childRect.width * childRect.height) / + const significantOverlap = + (childRect.width * childRect.height) / (parentRect.width * parentRect.height) > 0.5; if (fullyContained && significantOverlap) { @@ -203,8 +203,8 @@ export const getRect = async (page: Page, coordinates: Coordinates, listSelector const originalEl = document.elementFromPoint(x, y) as HTMLElement; if (originalEl) { let element = originalEl; - - const containerTags = ['DIV', 'SECTION', 'ARTICLE', 'MAIN', 'HEADER', 'FOOTER', 'NAV', 'ASIDE', + + const containerTags = ['DIV', 'SECTION', 'ARTICLE', 'MAIN', 'HEADER', 'FOOTER', 'NAV', 'ASIDE', 'ADDRESS', 'BLOCKQUOTE', 'DETAILS', 'DIALOG', 'FIGURE', 'FIGCAPTION', 'MAIN', 'MARK', 'SUMMARY', 'TIME', 'TABLE', 'THEAD', 'TBODY', 'TFOOT', 'TR', 'TH', 'TD', 'CAPTION', 'COLGROUP', 'COL', 'FORM', 'FIELDSET', 'LEGEND', 'LABEL', 'INPUT', 'BUTTON', 'SELECT', 'DATALIST', 'OPTGROUP', 'OPTION', 'TEXTAREA', 'OUTPUT', @@ -220,14 +220,14 @@ export const getRect = async (page: Page, coordinates: Coordinates, listSelector break; } - const fullyContained = + const fullyContained = parentRect.left <= childRect.left && parentRect.right >= childRect.right && parentRect.top <= childRect.top && parentRect.bottom >= childRect.bottom; - const significantOverlap = - (childRect.width * childRect.height) / + const significantOverlap = + (childRect.width * childRect.height) / (parentRect.width * parentRect.height) > 0.5; if (fullyContained && significantOverlap) { @@ -238,7 +238,7 @@ export const getRect = async (page: Page, coordinates: Coordinates, listSelector } const rectangle = element?.getBoundingClientRect(); - + if (rectangle) { return { x: rectangle.x, @@ -916,7 +916,7 @@ export const getNonUniqueSelectors = async (page: Page, coordinates: Coordinates let element = originalEl; - const containerTags = ['DIV', 'SECTION', 'ARTICLE', 'MAIN', 'HEADER', 'FOOTER', 'NAV', 'ASIDE', + const containerTags = ['DIV', 'SECTION', 'ARTICLE', 'MAIN', 'HEADER', 'FOOTER', 'NAV', 'ASIDE', 'ADDRESS', 'BLOCKQUOTE', 'DETAILS', 'DIALOG', 'FIGURE', 'FIGCAPTION', 'MAIN', 'MARK', 'SUMMARY', 'TIME', 'TABLE', 'THEAD', 'TBODY', 'TFOOT', 'TR', 'TH', 'TD', 'CAPTION', 'COLGROUP', 'COL', 'FORM', 'FIELDSET', 'LEGEND', 'LABEL', 'INPUT', 'BUTTON', 'SELECT', 'DATALIST', 'OPTGROUP', 'OPTION', 'TEXTAREA', 'OUTPUT', @@ -924,7 +924,7 @@ export const getNonUniqueSelectors = async (page: Page, coordinates: Coordinates 'PARAM', 'VIDEO', 'AUDIO', 'SOURCE', 'TRACK', 'CANVAS', 'MAP', 'AREA', 'SVG', 'IFRAME', 'FRAME', 'FRAMESET', 'LI', 'UL', 'OL', 'DL', 'DT', 'DD', 'HR', 'P', 'PRE', 'LISTING', 'PLAINTEXT', 'A' ]; - + while (element.parentElement) { const parentRect = element.parentElement.getBoundingClientRect(); const childRect = element.getBoundingClientRect(); @@ -933,22 +933,22 @@ export const getNonUniqueSelectors = async (page: Page, coordinates: Coordinates break; } - const fullyContained = + const fullyContained = parentRect.left <= childRect.left && parentRect.right >= childRect.right && parentRect.top <= childRect.top && parentRect.bottom >= childRect.bottom; - const significantOverlap = - (childRect.width * childRect.height) / + const significantOverlap = + (childRect.width * childRect.height) / (parentRect.width * parentRect.height) > 0.5; if (fullyContained && significantOverlap) { element = element.parentElement; } else { break; + } } - } const generalSelector = getSelectorPath(element); return { From 8533ea536291f4294037b5174f0471e6f03b5e0b Mon Sep 17 00:00:00 2001 From: amhsirak Date: Wed, 11 Dec 2024 02:02:37 +0530 Subject: [PATCH 04/88] chore: remove unused imprts --- server/src/workflow-management/selector.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index 66186a80..49d56f36 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -1,8 +1,7 @@ import { Page } from "playwright"; -import { Action, ActionType, Coordinates, TagName } from "../types"; +import { Coordinates } from "../types"; import { WhereWhatPair, WorkflowFile } from "maxun-core"; import logger from "../logger"; -import { getBestSelectorForAction } from "./utils"; /*TODO: 1. Handle TS errors (here we definetly know better) From 0b1b2436834bed1c6ed67dcbf8697e7c81fcdce3 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Wed, 11 Dec 2024 02:02:52 +0530 Subject: [PATCH 05/88] chore: remove todo --- server/src/workflow-management/selector.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index 49d56f36..5fc621ba 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -3,11 +3,6 @@ import { Coordinates } from "../types"; import { WhereWhatPair, WorkflowFile } from "maxun-core"; import logger from "../logger"; -/*TODO: -1. Handle TS errors (here we definetly know better) -2. Add pending function descriptions + thought process (esp. selector generation) -*/ - type Workflow = WorkflowFile["workflow"]; /** From fea0c0331b6d2f347471fd7627b1262402f12611 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Wed, 11 Dec 2024 02:04:16 +0530 Subject: [PATCH 06/88] chore: cleanup --- server/src/workflow-management/selector.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index 5fc621ba..4c6d3f87 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -21,7 +21,6 @@ export const getElementInformation = async ( ) => { try { if (listSelector !== '') { - // Old implementation const elementInfo = await page.evaluate( async ({ x, y }) => { const el = document.elementFromPoint(x, y) as HTMLElement; @@ -70,7 +69,6 @@ export const getElementInformation = async ( ); return elementInfo; } else { - // New implementation const elementInfo = await page.evaluate( async ({ x, y }) => { const originalEl = document.elementFromPoint(x, y) as HTMLElement; @@ -83,7 +81,7 @@ export const getElementInformation = async ( 'LEGEND', 'LABEL', 'INPUT', 'BUTTON', 'SELECT', 'DATALIST', 'OPTGROUP', 'OPTION', 'TEXTAREA', 'OUTPUT', 'PROGRESS', 'METER', 'DETAILS', 'SUMMARY', 'MENU', 'MENUITEM', 'MENUITEM', 'APPLET', 'EMBED', 'OBJECT', 'PARAM', 'VIDEO', 'AUDIO', 'SOURCE', 'TRACK', 'CANVAS', 'MAP', 'AREA', 'SVG', 'IFRAME', 'FRAME', 'FRAMESET', - 'LI', 'UL', 'OL', 'DL', 'DT', 'DD', 'HR', 'P', 'PRE', 'LISTING', 'PLAINTEXT' + 'LI', 'UL', 'OL', 'DL', 'DT', 'DD', 'HR', 'P', 'PRE', 'LISTING', 'PLAINTEXT', 'A' ]; while (element.parentElement) { const parentRect = element.parentElement.getBoundingClientRect(); @@ -164,7 +162,6 @@ export const getElementInformation = async ( export const getRect = async (page: Page, coordinates: Coordinates, listSelector: string) => { try { if (listSelector !== '') { - // Old implementation const rect = await page.evaluate( async ({ x, y }) => { const el = document.elementFromPoint(x, y) as HTMLElement; @@ -191,7 +188,6 @@ export const getRect = async (page: Page, coordinates: Coordinates, listSelector ); return rect; } else { - // New implementation const rect = await page.evaluate( async ({ x, y }) => { const originalEl = document.elementFromPoint(x, y) as HTMLElement; @@ -204,7 +200,7 @@ export const getRect = async (page: Page, coordinates: Coordinates, listSelector 'LEGEND', 'LABEL', 'INPUT', 'BUTTON', 'SELECT', 'DATALIST', 'OPTGROUP', 'OPTION', 'TEXTAREA', 'OUTPUT', 'PROGRESS', 'METER', 'DETAILS', 'SUMMARY', 'MENU', 'MENUITEM', 'MENUITEM', 'APPLET', 'EMBED', 'OBJECT', 'PARAM', 'VIDEO', 'AUDIO', 'SOURCE', 'TRACK', 'CANVAS', 'MAP', 'AREA', 'SVG', 'IFRAME', 'FRAME', 'FRAMESET', - 'LI', 'UL', 'OL', 'DL', 'DT', 'DD', 'HR', 'P', 'PRE', 'LISTING', 'PLAINTEXT' + 'LI', 'UL', 'OL', 'DL', 'DT', 'DD', 'HR', 'P', 'PRE', 'LISTING', 'PLAINTEXT', 'A' ]; while (element.parentElement) { const parentRect = element.parentElement.getBoundingClientRect(); From b72baca821ffec5ef3b31a231e356c82503fc489 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Wed, 11 Dec 2024 02:08:08 +0530 Subject: [PATCH 07/88] docs: re-add jsdoc --- server/src/workflow-management/selector.ts | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index 4c6d3f87..891c0e3b 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -6,13 +6,12 @@ import logger from "../logger"; type Workflow = WorkflowFile["workflow"]; /** - * Returns a {@link Rectangle} object representing - * the coordinates, width, height and corner points of the element. - * If an element is not found, returns null. + * 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} + * @returns {Promise} */ export const getElementInformation = async ( page: Page, @@ -159,6 +158,15 @@ export const getElementInformation = async ( } }; +/** + * 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, listSelector: string) => { try { if (listSelector !== '') { From d6e4b8860fd05e1abf471b6d85891d4a34e9a6bd Mon Sep 17 00:00:00 2001 From: amhsirak Date: Wed, 11 Dec 2024 02:12:43 +0530 Subject: [PATCH 08/88] chore: remove console logs --- server/src/workflow-management/classes/Generator.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/server/src/workflow-management/classes/Generator.ts b/server/src/workflow-management/classes/Generator.ts index 1e81cd4a..57be015e 100644 --- a/server/src/workflow-management/classes/Generator.ts +++ b/server/src/workflow-management/classes/Generator.ts @@ -542,9 +542,6 @@ export class WorkflowGenerator { */ private generateSelector = async (page: Page, coordinates: Coordinates, action: ActionType) => { const elementInfo = await getElementInformation(page, coordinates, this.listSelector); - - console.log(`List selector: ${this.listSelector}`) - const selectorBasedOnCustomAction = (this.getList === true) ? await getNonUniqueSelectors(page, coordinates) : await getSelectors(page, coordinates); @@ -580,8 +577,6 @@ export class WorkflowGenerator { if (this.listSelector !== '') { const childSelectors = await getChildSelectors(page, this.listSelector || ''); this.socket.emit('highlighter', { rect, selector: displaySelector, elementInfo, childSelectors }) - console.log(`Child Selectors: ${childSelectors}`) - console.log(`Parent Selector: ${this.listSelector}`) } else { this.socket.emit('highlighter', { rect, selector: displaySelector, elementInfo }); } From 2135f2eb6227fcb7fd86fd32155161b6d211e438 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Wed, 11 Dec 2024 13:57:09 +0530 Subject: [PATCH 09/88] chore: use node:22-slim as base --- server/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/Dockerfile b/server/Dockerfile index ae26e8eb..877b781e 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/playwright:v1.40.0-jammy +FROM node:22-slim # Set working directory WORKDIR /app From 08f8684b98dc2d1dc56c19c6805a71b4f444600b Mon Sep 17 00:00:00 2001 From: amhsirak Date: Wed, 11 Dec 2024 14:05:14 +0530 Subject: [PATCH 10/88] chore: use BACKEND_PORT --- server/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/Dockerfile b/server/Dockerfile index 877b781e..f8853fb2 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -50,7 +50,7 @@ RUN apt-get update && apt-get install -y \ # RUN chmod +x ./start.sh # Expose the backend port -EXPOSE 8080 +EXPOSE ${BACKEND_PORT:-8080} # Start the backend using the start script CMD ["npm", "run", "server"] \ No newline at end of file From a2b87762015eff62c439c130ce8144f2eeaf4a7a Mon Sep 17 00:00:00 2001 From: amhsirak Date: Wed, 11 Dec 2024 14:06:08 +0530 Subject: [PATCH 11/88] chore: use BACKEND_PORT --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 719e7814..032dc982 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,7 +16,7 @@ COPY vite.config.js ./ COPY tsconfig.json ./ # Expose the frontend port -EXPOSE 5173 +EXPOSE ${FRONTEND_PORT:-5173} # Start the frontend using the client script CMD ["npm", "run", "client", "--", "--host"] \ No newline at end of file From c2ce3f3387c87191d6d940354afff716272b2083 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Wed, 11 Dec 2024 14:13:12 +0530 Subject: [PATCH 12/88] feat: explicitly fetch easylist url --- server/src/browser-management/classes/RemoteBrowser.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/browser-management/classes/RemoteBrowser.ts b/server/src/browser-management/classes/RemoteBrowser.ts index 7e0a7d1a..71fdd933 100644 --- a/server/src/browser-management/classes/RemoteBrowser.ts +++ b/server/src/browser-management/classes/RemoteBrowser.ts @@ -180,7 +180,7 @@ export class RemoteBrowser { // await this.currentPage.setExtraHTTPHeaders({ // 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3' // }); - const blocker = await PlaywrightBlocker.fromPrebuiltAdsAndTracking(fetch); + const blocker = await PlaywrightBlocker.fromLists(fetch, ['https://easylist.to/easylist/easylist.txt']); await blocker.enableBlockingInPage(this.currentPage); this.client = await this.currentPage.context().newCDPSession(this.currentPage); await blocker.disableBlockingInPage(this.currentPage); From 44880cfce0ef8030ed9915beaff5b3fb806c3770 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Wed, 11 Dec 2024 14:14:23 +0530 Subject: [PATCH 13/88] feat: explicitly fetch easylist url --- maxun-core/src/interpret.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/maxun-core/src/interpret.ts b/maxun-core/src/interpret.ts index bd90e40f..1837dbd5 100644 --- a/maxun-core/src/interpret.ts +++ b/maxun-core/src/interpret.ts @@ -102,7 +102,7 @@ export default class Interpreter extends EventEmitter { }; } - PlaywrightBlocker.fromPrebuiltAdsAndTracking(fetch).then(blocker => { + PlaywrightBlocker.fromLists(fetch, ['https://easylist.to/easylist/easylist.txt']).then(blocker => { this.blocker = blocker; }).catch(err => { this.log(`Failed to initialize ad-blocker:`, Level.ERROR); From 53cb428715f468be6e696daa2cee12d781080a41 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Wed, 11 Dec 2024 14:21:26 +0530 Subject: [PATCH 14/88] chore: core v0.0.6 --- maxun-core/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/maxun-core/package.json b/maxun-core/package.json index 90ee01b7..36d06aa9 100644 --- a/maxun-core/package.json +++ b/maxun-core/package.json @@ -1,6 +1,6 @@ { "name": "maxun-core", - "version": "0.0.5", + "version": "0.0.6", "description": "Core package for Maxun, responsible for data extraction", "main": "build/index.js", "typings": "build/index.d.ts", From 63230480f3a42394f2d276ebd4044906da21ae2a Mon Sep 17 00:00:00 2001 From: amhsirak Date: Wed, 11 Dec 2024 14:22:05 +0530 Subject: [PATCH 15/88] chore: use core v0.0.6 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a7d634f3..6761d7a9 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "jwt-decode": "^4.0.0", "loglevel": "^1.8.0", "loglevel-plugin-remote": "^0.6.8", - "maxun-core": "^0.0.5", + "maxun-core": "0.0.6", "minio": "^8.0.1", "moment-timezone": "^0.5.45", "node-cron": "^3.0.3", From ab2c32c334224236243c65ee451187aaf3fea835 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Wed, 11 Dec 2024 14:34:09 +0530 Subject: [PATCH 16/88] chore: use core v0.0.6 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6761d7a9..9fab1c55 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "jwt-decode": "^4.0.0", "loglevel": "^1.8.0", "loglevel-plugin-remote": "^0.6.8", - "maxun-core": "0.0.6", + "maxun-core": "^0.0.6", "minio": "^8.0.1", "moment-timezone": "^0.5.45", "node-cron": "^3.0.3", From 337ad21577b5ec90ffcb6bd687f82f4fde6737b4 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Wed, 11 Dec 2024 14:53:18 +0530 Subject: [PATCH 17/88] feat: update description --- server/src/swagger/config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/swagger/config.ts b/server/src/swagger/config.ts index c9c12210..2c7c588e 100644 --- a/server/src/swagger/config.ts +++ b/server/src/swagger/config.ts @@ -7,7 +7,7 @@ const options = { info: { title: 'Maxun API Documentation', version: '1.0.0', - description: 'API documentation for Maxun (https://github.com/getmaxun/maxun)', + description: 'Maxun lets you get the data your robot extracted and run robots via API. All you need to do is input the Maxun API key by clicking Authorize below.', }, components: { securitySchemes: { From 2025d09e1e8240fd921e4d4dc6a722d630eeb42c Mon Sep 17 00:00:00 2001 From: amhsirak Date: Wed, 11 Dec 2024 14:53:54 +0530 Subject: [PATCH 18/88] feat: rename to website to api --- server/src/swagger/config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/swagger/config.ts b/server/src/swagger/config.ts index 2c7c588e..5f267053 100644 --- a/server/src/swagger/config.ts +++ b/server/src/swagger/config.ts @@ -5,7 +5,7 @@ const options = { definition: { openapi: '3.0.0', info: { - title: 'Maxun API Documentation', + title: 'Website to API', version: '1.0.0', description: 'Maxun lets you get the data your robot extracted and run robots via API. All you need to do is input the Maxun API key by clicking Authorize below.', }, From 319f9fce24e22576b98639f91a09e7075edf0940 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Wed, 11 Dec 2024 15:16:45 +0530 Subject: [PATCH 19/88] feat: ensure swagger is accessible with or without build --- server/src/swagger/config.ts | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/server/src/swagger/config.ts b/server/src/swagger/config.ts index 5f267053..bf115ed1 100644 --- a/server/src/swagger/config.ts +++ b/server/src/swagger/config.ts @@ -1,5 +1,16 @@ import swaggerJSDoc from 'swagger-jsdoc'; import path from 'path'; +import fs from 'fs'; + +// Dynamically determine API file paths +const jsFiles = [path.join(__dirname, '../api/*.js')] +const tsFiles = [path.join(__dirname, '../api/*.ts')] + +let apis = fs.existsSync(jsFiles[0]) ? jsFiles : tsFiles; + +if (!apis) { + throw new Error('No valid API files found! Ensure either .js or .ts files exist in the ../api/ directory.'); +} const options = { definition: { @@ -7,7 +18,8 @@ const options = { info: { title: 'Website to API', version: '1.0.0', - description: 'Maxun lets you get the data your robot extracted and run robots via API. All you need to do is input the Maxun API key by clicking Authorize below.', + description: + 'Maxun lets you get the data your robot extracted and run robots via API. All you need to do is input the Maxun API key by clicking Authorize below.', }, components: { securitySchemes: { @@ -15,7 +27,8 @@ const options = { type: 'apiKey', in: 'header', name: 'x-api-key', - description: 'API key for authorization. You can find your API key in the "API Key" section on Maxun Dashboard.', + description: + 'API key for authorization. You can find your API key in the "API Key" section on Maxun Dashboard.', }, }, }, @@ -25,7 +38,7 @@ const options = { }, ], }, - apis: process.env.NODE_ENV === 'production' ? [path.join(__dirname, '../api/*.js')] : [path.join(__dirname, '../api/*.ts')] + apis, }; const swaggerSpec = swaggerJSDoc(options); From 62198377d67cdb136d9e1695b2aed68154a5f34f Mon Sep 17 00:00:00 2001 From: amhsirak Date: Wed, 11 Dec 2024 15:29:05 +0530 Subject: [PATCH 20/88] chore: 0.0.6 BE img, 0.0.3 FE img --- docker-compose.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 46cc72c4..63e59187 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -43,7 +43,7 @@ services: #build: #context: . #dockerfile: server/Dockerfile - image: getmaxun/maxun-backend:v0.0.5 + image: getmaxun/maxun-backend:v0.0.6 ports: - "${BACKEND_PORT:-8080}:${BACKEND_PORT:-8080}" env_file: .env @@ -72,7 +72,7 @@ services: #build: #context: . #dockerfile: Dockerfile - image: getmaxun/maxun-frontend:v0.0.2 + image: getmaxun/maxun-frontend:v0.0.3 ports: - "${FRONTEND_PORT:-5173}:${FRONTEND_PORT:-5173}" env_file: .env From 8b107f0685d4c0ba8df3f408ad7901ef3fb88f07 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Wed, 11 Dec 2024 16:54:34 +0530 Subject: [PATCH 21/88] feat: use latest docker image --- server/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/Dockerfile b/server/Dockerfile index f8853fb2..e738f252 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -1,4 +1,4 @@ -FROM node:22-slim +FROM mcr.microsoft.com/playwright:v1.46.0-noble # Set working directory WORKDIR /app From 8b33157f53ddebe3b0d5c385e14fcc712ac2d22d Mon Sep 17 00:00:00 2001 From: amhsirak Date: Wed, 11 Dec 2024 16:55:10 +0530 Subject: [PATCH 22/88] chore: 0.0.7 BE img --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 63e59187..51a9f4eb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -43,7 +43,7 @@ services: #build: #context: . #dockerfile: server/Dockerfile - image: getmaxun/maxun-backend:v0.0.6 + image: getmaxun/maxun-backend:v0.0.7 ports: - "${BACKEND_PORT:-8080}:${BACKEND_PORT:-8080}" env_file: .env From 18b517c875d00b5856a26118b55a9d7c614cd152 Mon Sep 17 00:00:00 2001 From: Karishma Shukla Date: Thu, 12 Dec 2024 18:41:25 +0530 Subject: [PATCH 23/88] chore: remove note --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index ac74d21c..1fc1bfd6 100644 --- a/README.md +++ b/README.md @@ -29,9 +29,7 @@ Maxun lets you train a robot in 2 minutes and scrape the web on auto-pilot. Web -> Note: Maxun is in its early stages of development and currently does not support self-hosting. However, you can run Maxun locally. Self-hosting capabilities are planned for a future release and will be available soon. - -# Local Installation +# Installation ### Docker Compose ``` git clone https://github.com/getmaxun/maxun From ac13cd81d95f36dfd709c5177a6f857920e50467 Mon Sep 17 00:00:00 2001 From: Karishma Shukla Date: Thu, 12 Dec 2024 18:41:48 +0530 Subject: [PATCH 24/88] chore: remove clone instruction --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 1fc1bfd6..26eb72fe 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,6 @@ Maxun lets you train a robot in 2 minutes and scrape the web on auto-pilot. Web # Installation ### Docker Compose ``` -git clone https://github.com/getmaxun/maxun docker-compose up -d ``` You can access the frontend at http://localhost:5173/ and backend at http://localhost:8080/ From 171d669d2ed69249c1336311493ed8cf906575e2 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Fri, 13 Dec 2024 21:20:25 +0530 Subject: [PATCH 25/88] feat: pass context script for webdriver --- .../classes/RemoteBrowser.ts | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/server/src/browser-management/classes/RemoteBrowser.ts b/server/src/browser-management/classes/RemoteBrowser.ts index 04fb59b3..2dab103a 100644 --- a/server/src/browser-management/classes/RemoteBrowser.ts +++ b/server/src/browser-management/classes/RemoteBrowser.ts @@ -225,6 +225,31 @@ export class RemoteBrowser { contextOptions.userAgent = browserUserAgent; this.context = await this.browser.newContext(contextOptions); + await this.context.addInitScript( + `const defaultGetter = Object.getOwnPropertyDescriptor( + Navigator.prototype, + "webdriver" + ).get; + defaultGetter.apply(navigator); + defaultGetter.toString(); + Object.defineProperty(Navigator.prototype, "webdriver", { + set: undefined, + enumerable: true, + configurable: true, + get: new Proxy(defaultGetter, { + apply: (target, thisArg, args) => { + Reflect.apply(target, thisArg, args); + return false; + }, + }), + }); + const patchedGetter = Object.getOwnPropertyDescriptor( + Navigator.prototype, + "webdriver" + ).get; + patchedGetter.apply(navigator); + patchedGetter.toString();` + ); this.currentPage = await this.context.newPage(); await this.setupPageEventListeners(this.currentPage); From cd05ddfb3c780aa26ec1e6108c4f173dafe26291 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Fri, 13 Dec 2024 21:21:00 +0530 Subject: [PATCH 26/88] chore: lint --- .../src/browser-management/classes/RemoteBrowser.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/server/src/browser-management/classes/RemoteBrowser.ts b/server/src/browser-management/classes/RemoteBrowser.ts index 2dab103a..b19b5cbc 100644 --- a/server/src/browser-management/classes/RemoteBrowser.ts +++ b/server/src/browser-management/classes/RemoteBrowser.ts @@ -104,7 +104,7 @@ export class RemoteBrowser { } catch { return url; } - } + } /** * Determines if a URL change is significant enough to emit @@ -130,11 +130,11 @@ export class RemoteBrowser { }); // Handle page load events with retry mechanism - page.on('load', async () => { + page.on('load', async () => { const injectScript = async (): Promise => { try { await page.waitForLoadState('networkidle', { timeout: 5000 }); - + await page.evaluate(getInjectableScript()); return true; } catch (error: any) { @@ -201,7 +201,7 @@ export class RemoteBrowser { const contextOptions: any = { viewport: { height: 400, width: 900 }, // recordVideo: { dir: 'videos/' } - // Force reduced motion to prevent animation issues + // Force reduced motion to prevent animation issues reducedMotion: 'reduce', // Force JavaScript to be enabled javaScriptEnabled: true, @@ -249,7 +249,7 @@ export class RemoteBrowser { ).get; patchedGetter.apply(navigator); patchedGetter.toString();` - ); + ); this.currentPage = await this.context.newPage(); await this.setupPageEventListeners(this.currentPage); @@ -481,7 +481,7 @@ export class RemoteBrowser { this.currentPage = newPage; if (this.currentPage) { await this.setupPageEventListeners(this.currentPage); - + this.client = await this.currentPage.context().newCDPSession(this.currentPage); await this.subscribeToScreencast(); } else { From c49e70a1eeab9ef569f1e41738bea8882915ec22 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Fri, 13 Dec 2024 22:04:35 +0530 Subject: [PATCH 27/88] chrome and chromium user agent --- server/src/browser-management/classes/RemoteBrowser.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/server/src/browser-management/classes/RemoteBrowser.ts b/server/src/browser-management/classes/RemoteBrowser.ts index b19b5cbc..e5d3217e 100644 --- a/server/src/browser-management/classes/RemoteBrowser.ts +++ b/server/src/browser-management/classes/RemoteBrowser.ts @@ -220,8 +220,7 @@ export class RemoteBrowser { password: proxyOptions.password ? proxyOptions.password : undefined, }; } - const browserUserAgent = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.5481.38 Safari/537.36"; - + const browserUserAgent = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.140 Chromium/131.0.6778.140 Safari/537.36"; contextOptions.userAgent = browserUserAgent; this.context = await this.browser.newContext(contextOptions); From b173ce3e98d9bcc2838de7ee257f3b8ee5e95a92 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Fri, 13 Dec 2024 22:05:51 +0530 Subject: [PATCH 28/88] chore: remove commented code --- .../classes/RemoteBrowser.ts | 32 ------------------- 1 file changed, 32 deletions(-) diff --git a/server/src/browser-management/classes/RemoteBrowser.ts b/server/src/browser-management/classes/RemoteBrowser.ts index e5d3217e..0299faf6 100644 --- a/server/src/browser-management/classes/RemoteBrowser.ts +++ b/server/src/browser-management/classes/RemoteBrowser.ts @@ -155,35 +155,6 @@ export class RemoteBrowser { * @returns {Promise} */ public initialize = async (userId: string): Promise => { - // const launchOptions = { - // headless: true, - // proxy: options.launchOptions?.proxy, - // chromiumSandbox: false, - // args: [ - // '--no-sandbox', - // '--disable-setuid-sandbox', - // '--headless=new', - // '--disable-gpu', - // '--disable-dev-shm-usage', - // '--disable-software-rasterizer', - // '--in-process-gpu', - // '--disable-infobars', - // '--single-process', - // '--no-zygote', - // '--disable-notifications', - // '--disable-extensions', - // '--disable-background-timer-throttling', - // ...(options.launchOptions?.args || []) - // ], - // env: { - // ...process.env, - // CHROMIUM_FLAGS: '--disable-gpu --no-sandbox --headless=new' - // } - // }; - // console.log('Launch options before:', options.launchOptions); - // this.browser = (await options.browser.launch(launchOptions)); - - // console.log('Launch options after:', options.launchOptions) this.browser = (await chromium.launch({ headless: true, })); @@ -253,9 +224,6 @@ export class RemoteBrowser { await this.setupPageEventListeners(this.currentPage); - // await this.currentPage.setExtraHTTPHeaders({ - // 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3' - // }); const blocker = await PlaywrightBlocker.fromLists(fetch, ['https://easylist.to/easylist/easylist.txt']); await blocker.enableBlockingInPage(this.currentPage); this.client = await this.currentPage.context().newCDPSession(this.currentPage); From 06184010ae1a789c86bc80d00cd082752fdd1444 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Sat, 14 Dec 2024 06:58:29 +0530 Subject: [PATCH 29/88] feat: args --- server/src/browser-management/classes/RemoteBrowser.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/server/src/browser-management/classes/RemoteBrowser.ts b/server/src/browser-management/classes/RemoteBrowser.ts index 0299faf6..9dc51690 100644 --- a/server/src/browser-management/classes/RemoteBrowser.ts +++ b/server/src/browser-management/classes/RemoteBrowser.ts @@ -157,6 +157,13 @@ export class RemoteBrowser { public initialize = async (userId: string): Promise => { this.browser = (await chromium.launch({ headless: true, + args: [ + "--disable-blink-features=AutomationControlled", + "--disable-web-security", + "--disable-features=IsolateOrigins,site-per-process", + "--disable-site-isolation-trials", + "--disable-extensions" + ], })); const proxyConfig = await getDecryptedProxyConfig(userId); let proxyOptions: { server: string, username?: string, password?: string } = { server: '' }; From e5e46e8e6270ff47387efb4699940d6661e3217e Mon Sep 17 00:00:00 2001 From: Karishma Shukla Date: Sat, 14 Dec 2024 07:43:19 +0530 Subject: [PATCH 30/88] docs: clearer installation instructions --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 26eb72fe..c8441f1a 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,14 @@ Maxun lets you train a robot in 2 minutes and scrape the web on auto-pilot. Web # Installation +1. First, create a file named `.env` in the root folder of the project +2. Example env file can be viewed [here](https://github.com/getmaxun/maxun/blob/master/ENVEXAMPLE). Copy all content of example env to your `.env` file. +3. Choose your installation method below + ### Docker Compose +1. Copy the `docker-compose.yml` file from the codebase +2. Ensure you have setup the `.env` file +3. Run the command below ``` docker-compose up -d ``` From eb27cddacd27c4b9bcc73c840d909ef9a2b42bb9 Mon Sep 17 00:00:00 2001 From: Karishma Shukla Date: Sat, 14 Dec 2024 07:47:03 +0530 Subject: [PATCH 31/88] docs: compose file location --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c8441f1a..38d0eebe 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ Maxun lets you train a robot in 2 minutes and scrape the web on auto-pilot. Web 3. Choose your installation method below ### Docker Compose -1. Copy the `docker-compose.yml` file from the codebase +1. Copy paste the [docker-compose.yml file](https://github.com/getmaxun/maxun/blob/master/docker-compose.yml) 2. Ensure you have setup the `.env` file 3. Run the command below ``` From 0284f3d001536ec7128c484044fbac102647e707 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Sat, 14 Dec 2024 09:27:13 +0530 Subject: [PATCH 32/88] feat: remove fe be mounts --- docker-compose.yml | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 51a9f4eb..3c6e3a0f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -64,8 +64,6 @@ services: - redis - minio volumes: - - ./server:/app/server # Mount server source code for hot reloading - - ./maxun-core:/app/maxun-core # Mount maxun-core for any shared code updates - /var/run/dbus:/var/run/dbus frontend: @@ -79,13 +77,10 @@ services: environment: PUBLIC_URL: ${PUBLIC_URL} BACKEND_URL: ${BACKEND_URL} - volumes: - - ./:/app # Mount entire frontend app directory for hot reloading - - /app/node_modules # Anonymous volume to prevent overwriting node_modules depends_on: - backend volumes: postgres_data: minio_data: - redis_data: + redis_data: \ No newline at end of file From 44693259257497deb6b285f54e64a0ae0ec2b7b1 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Sat, 14 Dec 2024 10:49:52 +0530 Subject: [PATCH 33/88] chore: sync compose master <-> develop --- docker-compose.yml | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 51a9f4eb..3c6e3a0f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -64,8 +64,6 @@ services: - redis - minio volumes: - - ./server:/app/server # Mount server source code for hot reloading - - ./maxun-core:/app/maxun-core # Mount maxun-core for any shared code updates - /var/run/dbus:/var/run/dbus frontend: @@ -79,13 +77,10 @@ services: environment: PUBLIC_URL: ${PUBLIC_URL} BACKEND_URL: ${BACKEND_URL} - volumes: - - ./:/app # Mount entire frontend app directory for hot reloading - - /app/node_modules # Anonymous volume to prevent overwriting node_modules depends_on: - backend volumes: postgres_data: minio_data: - redis_data: + redis_data: \ No newline at end of file From 7f48464eea993f0d4468942cdeb77c87398191f8 Mon Sep 17 00:00:00 2001 From: RohitR311 Date: Sat, 14 Dec 2024 18:35:38 +0530 Subject: [PATCH 34/88] feat: add page navigation timeout --- maxun-core/src/interpret.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/maxun-core/src/interpret.ts b/maxun-core/src/interpret.ts index d1cc8318..848ddd76 100644 --- a/maxun-core/src/interpret.ts +++ b/maxun-core/src/interpret.ts @@ -365,7 +365,7 @@ export default class Interpreter extends EventEmitter { try { const newPage = await context.newPage(); await newPage.goto(link); - await newPage.waitForLoadState('networkidle'); + await newPage.waitForLoadState('domcontentloaded'); await this.runLoop(newPage, this.initializedWorkflow!); } catch (e) { // `runLoop` uses soft mode, so it recovers from it's own exceptions @@ -576,7 +576,7 @@ export default class Interpreter extends EventEmitter { } await Promise.all([ nextButton.dispatchEvent('click'), - page.waitForNavigation({ waitUntil: 'networkidle' }) + page.waitForNavigation({ waitUntil: 'domcontentloaded' }) ]); await page.waitForTimeout(1000); @@ -767,6 +767,8 @@ export default class Interpreter extends EventEmitter { public async run(page: Page, params?: ParamType): Promise { this.log('Starting the workflow.', Level.LOG); const context = page.context(); + + page.setDefaultNavigationTimeout(100000); // Check proxy settings from context options const contextOptions = (context as any)._options; From bdf908e37cdcb2200cb5c653a5149db279ce51aa Mon Sep 17 00:00:00 2001 From: RohitR311 Date: Sat, 14 Dec 2024 18:36:59 +0530 Subject: [PATCH 35/88] feat: add domcontentloaded wait load state --- server/src/workflow-management/classes/Generator.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/src/workflow-management/classes/Generator.ts b/server/src/workflow-management/classes/Generator.ts index 57be015e..2cde90e4 100644 --- a/server/src/workflow-management/classes/Generator.ts +++ b/server/src/workflow-management/classes/Generator.ts @@ -189,7 +189,7 @@ export class WorkflowGenerator { * * This function also makes sure to add a waitForLoadState and a generated flag * action after every new action or pair added. The [waitForLoadState](https://playwright.dev/docs/api/class-frame#frame-wait-for-load-state) - * action waits for the networkidle event to be fired, + * action waits for the domcontentloaded event to be fired, * and the generated flag action is used for making pausing the interpretation possible. * * @param pair The pair to add to the workflow. @@ -217,7 +217,7 @@ export class WorkflowGenerator { if (pair.what[0].action !== 'waitForLoadState' && pair.what[0].action !== 'press') { pair.what.push({ action: 'waitForLoadState', - args: ['networkidle'], + args: ['domcontentloaded'], }); } this.workflowRecord.workflow[matchedIndex].what = this.workflowRecord.workflow[matchedIndex].what.concat(pair.what); @@ -232,7 +232,7 @@ export class WorkflowGenerator { if (pair.what[0].action !== 'waitForLoadState' && pair.what[0].action !== 'press') { pair.what.push({ action: 'waitForLoadState', - args: ['networkidle'], + args: ['domcontentloaded'], }); } if (this.generatedData.lastIndex === 0) { From f38230d1b4d45d266886679c3228d07d2f52d18d Mon Sep 17 00:00:00 2001 From: RohitR311 Date: Sat, 14 Dec 2024 20:30:24 +0530 Subject: [PATCH 36/88] feat: revert to networkidle for wait load state --- server/src/workflow-management/classes/Generator.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/src/workflow-management/classes/Generator.ts b/server/src/workflow-management/classes/Generator.ts index 2cde90e4..57be015e 100644 --- a/server/src/workflow-management/classes/Generator.ts +++ b/server/src/workflow-management/classes/Generator.ts @@ -189,7 +189,7 @@ export class WorkflowGenerator { * * This function also makes sure to add a waitForLoadState and a generated flag * action after every new action or pair added. The [waitForLoadState](https://playwright.dev/docs/api/class-frame#frame-wait-for-load-state) - * action waits for the domcontentloaded event to be fired, + * action waits for the networkidle event to be fired, * and the generated flag action is used for making pausing the interpretation possible. * * @param pair The pair to add to the workflow. @@ -217,7 +217,7 @@ export class WorkflowGenerator { if (pair.what[0].action !== 'waitForLoadState' && pair.what[0].action !== 'press') { pair.what.push({ action: 'waitForLoadState', - args: ['domcontentloaded'], + args: ['networkidle'], }); } this.workflowRecord.workflow[matchedIndex].what = this.workflowRecord.workflow[matchedIndex].what.concat(pair.what); @@ -232,7 +232,7 @@ export class WorkflowGenerator { if (pair.what[0].action !== 'waitForLoadState' && pair.what[0].action !== 'press') { pair.what.push({ action: 'waitForLoadState', - args: ['domcontentloaded'], + args: ['networkidle'], }); } if (this.generatedData.lastIndex === 0) { From 7ce7a1598c3c394d8107677859991257460755ee Mon Sep 17 00:00:00 2001 From: RohitR311 Date: Sat, 14 Dec 2024 20:32:07 +0530 Subject: [PATCH 37/88] feat: check for selector visibility in getState --- maxun-core/src/interpret.ts | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/maxun-core/src/interpret.ts b/maxun-core/src/interpret.ts index 848ddd76..e11ae255 100644 --- a/maxun-core/src/interpret.ts +++ b/maxun-core/src/interpret.ts @@ -192,8 +192,8 @@ export default class Interpreter extends EventEmitter { // const actionable = async (selector: string): Promise => { // try { // const proms = [ - // page.isEnabled(selector, { timeout: 5000 }), - // page.isVisible(selector, { timeout: 5000 }), + // page.isEnabled(selector, { timeout: 10000 }), + // page.isVisible(selector, { timeout: 10000 }), // ]; // return await Promise.all(proms).then((bools) => bools.every((x) => x)); @@ -214,6 +214,17 @@ export default class Interpreter extends EventEmitter { // return []; // }), // ).then((x) => x.flat()); + + const presentSelectors: SelectorArray = await Promise.all( + selectors.map(async (selector) => { + try { + await page.waitForSelector(selector, { state: 'attached' }); + return [selector]; + } catch (e) { + return []; + } + }), + ).then((x) => x.flat()); const action = workflowCopy[workflowCopy.length - 1]; @@ -233,7 +244,7 @@ export default class Interpreter extends EventEmitter { ...p, [cookie.name]: cookie.value, }), {}), - selectors, + selectors: presentSelectors, }; } @@ -365,7 +376,7 @@ export default class Interpreter extends EventEmitter { try { const newPage = await context.newPage(); await newPage.goto(link); - await newPage.waitForLoadState('domcontentloaded'); + await newPage.waitForLoadState('networkidle'); await this.runLoop(newPage, this.initializedWorkflow!); } catch (e) { // `runLoop` uses soft mode, so it recovers from it's own exceptions @@ -576,7 +587,7 @@ export default class Interpreter extends EventEmitter { } await Promise.all([ nextButton.dispatchEvent('click'), - page.waitForNavigation({ waitUntil: 'domcontentloaded' }) + page.waitForNavigation({ waitUntil: 'networkidle' }) ]); await page.waitForTimeout(1000); From e22c019a0c6feed2b2c3e2ecb2aa4b749dd0f226 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Sat, 14 Dec 2024 22:30:50 +0530 Subject: [PATCH 38/88] feat: rotate user agents --- .../browser-management/classes/RemoteBrowser.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/server/src/browser-management/classes/RemoteBrowser.ts b/server/src/browser-management/classes/RemoteBrowser.ts index 9dc51690..05927b24 100644 --- a/server/src/browser-management/classes/RemoteBrowser.ts +++ b/server/src/browser-management/classes/RemoteBrowser.ts @@ -148,6 +148,19 @@ export class RemoteBrowser { }); } + private getUserAgent() { + const userAgents = [ + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.5845.140 Safari/537.36', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:117.0) Gecko/20100101 Firefox/117.0', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.1938.81 Safari/537.36 Edg/116.0.1938.81', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.5845.96 Safari/537.36 OPR/101.0.4843.25', + 'Mozilla/5.0 (Windows NT 11.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.5938.62 Safari/537.36', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:118.0) Gecko/20100101 Firefox/118.0', + ]; + + return userAgents[Math.floor(Math.random() * userAgents.length)]; + } + /** * An asynchronous constructor for asynchronously initialized properties. * Must be called right after creating an instance of RemoteBrowser class. @@ -198,9 +211,8 @@ export class RemoteBrowser { password: proxyOptions.password ? proxyOptions.password : undefined, }; } - const browserUserAgent = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.140 Chromium/131.0.6778.140 Safari/537.36"; - contextOptions.userAgent = browserUserAgent; + contextOptions.userAgent = this.getUserAgent(); this.context = await this.browser.newContext(contextOptions); await this.context.addInitScript( `const defaultGetter = Object.getOwnPropertyDescriptor( From 6c15df78161969fdc46ea0af98fdcc19e2a5ed4f Mon Sep 17 00:00:00 2001 From: The Hague Centre for Strategic Studies <51345661+HCSS-StratBase@users.noreply.github.com> Date: Sat, 14 Dec 2024 19:46:21 +0100 Subject: [PATCH 39/88] Update README.md small addition to make the installation instructions more 'noob-proof' --- README.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 38d0eebe..b63cf8a7 100644 --- a/README.md +++ b/README.md @@ -30,14 +30,15 @@ Maxun lets you train a robot in 2 minutes and scrape the web on auto-pilot. Web # Installation -1. First, create a file named `.env` in the root folder of the project -2. Example env file can be viewed [here](https://github.com/getmaxun/maxun/blob/master/ENVEXAMPLE). Copy all content of example env to your `.env` file. -3. Choose your installation method below +1. Create a root folder for your project (e.g. 'maxun') +2. Create a file named `.env` in the root folder of the project +3. Example env file can be viewed [here](https://github.com/getmaxun/maxun/blob/master/ENVEXAMPLE). Copy all content of example env to your `.env` file. +4. Choose your installation method below ### Docker Compose -1. Copy paste the [docker-compose.yml file](https://github.com/getmaxun/maxun/blob/master/docker-compose.yml) -2. Ensure you have setup the `.env` file -3. Run the command below +1. Copy paste the [docker-compose.yml file](https://github.com/getmaxun/maxun/blob/master/docker-compose.yml) into your root folder +2. Ensure you have setup the `.env` file in that same folder +3. Run the command below from a terminal ``` docker-compose up -d ``` From 320f24ec002256067c418f45efbb48258599c893 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Sun, 15 Dec 2024 01:06:23 +0530 Subject: [PATCH 40/88] feat: shm & sandbox args --- server/src/browser-management/classes/RemoteBrowser.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/server/src/browser-management/classes/RemoteBrowser.ts b/server/src/browser-management/classes/RemoteBrowser.ts index 05927b24..4b059cda 100644 --- a/server/src/browser-management/classes/RemoteBrowser.ts +++ b/server/src/browser-management/classes/RemoteBrowser.ts @@ -175,7 +175,9 @@ export class RemoteBrowser { "--disable-web-security", "--disable-features=IsolateOrigins,site-per-process", "--disable-site-isolation-trials", - "--disable-extensions" + "--disable-extensions", + "--no-sandbox", + "--disable-dev-shm-usage", ], })); const proxyConfig = await getDecryptedProxyConfig(userId); @@ -201,7 +203,7 @@ export class RemoteBrowser { // Disable hardware acceleration forcedColors: 'none', isMobile: false, - hasTouch: false + hasTouch: false, }; if (proxyOptions.server) { @@ -212,7 +214,6 @@ export class RemoteBrowser { }; } - contextOptions.userAgent = this.getUserAgent(); this.context = await this.browser.newContext(contextOptions); await this.context.addInitScript( `const defaultGetter = Object.getOwnPropertyDescriptor( From ffe87b0c7db7b0e6446c3f0fb2a5d67e313f29b8 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Sun, 15 Dec 2024 01:06:45 +0530 Subject: [PATCH 41/88] feat: user getUserAgent() --- server/src/browser-management/classes/RemoteBrowser.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/server/src/browser-management/classes/RemoteBrowser.ts b/server/src/browser-management/classes/RemoteBrowser.ts index 4b059cda..31aceada 100644 --- a/server/src/browser-management/classes/RemoteBrowser.ts +++ b/server/src/browser-management/classes/RemoteBrowser.ts @@ -204,6 +204,7 @@ export class RemoteBrowser { forcedColors: 'none', isMobile: false, hasTouch: false, + userAgent: this.getUserAgent(), }; if (proxyOptions.server) { From e70145219eca9a092487db350487f4a6bb711906 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Sun, 15 Dec 2024 04:58:13 +0530 Subject: [PATCH 42/88] feat: remove container tags --- server/src/workflow-management/selector.ts | 37 ---------------------- 1 file changed, 37 deletions(-) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index 891c0e3b..bde38300 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -74,22 +74,10 @@ export const getElementInformation = async ( if (originalEl) { let element = originalEl; - const containerTags = ['DIV', 'SECTION', 'ARTICLE', 'MAIN', 'HEADER', 'FOOTER', 'NAV', 'ASIDE', - 'ADDRESS', 'BLOCKQUOTE', 'DETAILS', 'DIALOG', 'FIGURE', 'FIGCAPTION', 'MAIN', 'MARK', 'SUMMARY', 'TIME', - 'TABLE', 'THEAD', 'TBODY', 'TFOOT', 'TR', 'TH', 'TD', 'CAPTION', 'COLGROUP', 'COL', 'FORM', 'FIELDSET', - 'LEGEND', 'LABEL', 'INPUT', 'BUTTON', 'SELECT', 'DATALIST', 'OPTGROUP', 'OPTION', 'TEXTAREA', 'OUTPUT', - 'PROGRESS', 'METER', 'DETAILS', 'SUMMARY', 'MENU', 'MENUITEM', 'MENUITEM', 'APPLET', 'EMBED', 'OBJECT', - 'PARAM', 'VIDEO', 'AUDIO', 'SOURCE', 'TRACK', 'CANVAS', 'MAP', 'AREA', 'SVG', 'IFRAME', 'FRAME', 'FRAMESET', - 'LI', 'UL', 'OL', 'DL', 'DT', 'DD', 'HR', 'P', 'PRE', 'LISTING', 'PLAINTEXT', 'A' - ]; while (element.parentElement) { const parentRect = element.parentElement.getBoundingClientRect(); const childRect = element.getBoundingClientRect(); - if (!containerTags.includes(element.parentElement.tagName)) { - break; - } - const fullyContained = parentRect.left <= childRect.left && parentRect.right >= childRect.right && @@ -202,22 +190,10 @@ export const getRect = async (page: Page, coordinates: Coordinates, listSelector if (originalEl) { let element = originalEl; - const containerTags = ['DIV', 'SECTION', 'ARTICLE', 'MAIN', 'HEADER', 'FOOTER', 'NAV', 'ASIDE', - 'ADDRESS', 'BLOCKQUOTE', 'DETAILS', 'DIALOG', 'FIGURE', 'FIGCAPTION', 'MAIN', 'MARK', 'SUMMARY', 'TIME', - 'TABLE', 'THEAD', 'TBODY', 'TFOOT', 'TR', 'TH', 'TD', 'CAPTION', 'COLGROUP', 'COL', 'FORM', 'FIELDSET', - 'LEGEND', 'LABEL', 'INPUT', 'BUTTON', 'SELECT', 'DATALIST', 'OPTGROUP', 'OPTION', 'TEXTAREA', 'OUTPUT', - 'PROGRESS', 'METER', 'DETAILS', 'SUMMARY', 'MENU', 'MENUITEM', 'MENUITEM', 'APPLET', 'EMBED', 'OBJECT', - 'PARAM', 'VIDEO', 'AUDIO', 'SOURCE', 'TRACK', 'CANVAS', 'MAP', 'AREA', 'SVG', 'IFRAME', 'FRAME', 'FRAMESET', - 'LI', 'UL', 'OL', 'DL', 'DT', 'DD', 'HR', 'P', 'PRE', 'LISTING', 'PLAINTEXT', 'A' - ]; while (element.parentElement) { const parentRect = element.parentElement.getBoundingClientRect(); const childRect = element.getBoundingClientRect(); - if (!containerTags.includes(element.parentElement.tagName)) { - break; - } - const fullyContained = parentRect.left <= childRect.left && parentRect.right >= childRect.right && @@ -914,23 +890,10 @@ export const getNonUniqueSelectors = async (page: Page, coordinates: Coordinates let element = originalEl; - const containerTags = ['DIV', 'SECTION', 'ARTICLE', 'MAIN', 'HEADER', 'FOOTER', 'NAV', 'ASIDE', - 'ADDRESS', 'BLOCKQUOTE', 'DETAILS', 'DIALOG', 'FIGURE', 'FIGCAPTION', 'MAIN', 'MARK', 'SUMMARY', 'TIME', - 'TABLE', 'THEAD', 'TBODY', 'TFOOT', 'TR', 'TH', 'TD', 'CAPTION', 'COLGROUP', 'COL', 'FORM', 'FIELDSET', - 'LEGEND', 'LABEL', 'INPUT', 'BUTTON', 'SELECT', 'DATALIST', 'OPTGROUP', 'OPTION', 'TEXTAREA', 'OUTPUT', - 'PROGRESS', 'METER', 'DETAILS', 'SUMMARY', 'MENU', 'MENUITEM', 'MENUITEM', 'APPLET', 'EMBED', 'OBJECT', - 'PARAM', 'VIDEO', 'AUDIO', 'SOURCE', 'TRACK', 'CANVAS', 'MAP', 'AREA', 'SVG', 'IFRAME', 'FRAME', 'FRAMESET', - 'LI', 'UL', 'OL', 'DL', 'DT', 'DD', 'HR', 'P', 'PRE', 'LISTING', 'PLAINTEXT', 'A' - ]; - while (element.parentElement) { const parentRect = element.parentElement.getBoundingClientRect(); const childRect = element.getBoundingClientRect(); - if (!containerTags.includes(element.parentElement.tagName)) { - break; - } - const fullyContained = parentRect.left <= childRect.left && parentRect.right >= childRect.right && From cb0965323e3a8f13483e198bb1e7dbf086a4481d Mon Sep 17 00:00:00 2001 From: amhsirak Date: Sun, 15 Dec 2024 05:01:15 +0530 Subject: [PATCH 43/88] feat: accept getList in getRect and getElementInfo --- server/src/workflow-management/classes/Generator.ts | 6 +++--- server/src/workflow-management/selector.ts | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/server/src/workflow-management/classes/Generator.ts b/server/src/workflow-management/classes/Generator.ts index 57be015e..2ab54753 100644 --- a/server/src/workflow-management/classes/Generator.ts +++ b/server/src/workflow-management/classes/Generator.ts @@ -541,7 +541,7 @@ export class WorkflowGenerator { * @returns {Promise} */ private generateSelector = async (page: Page, coordinates: Coordinates, action: ActionType) => { - const elementInfo = await getElementInformation(page, coordinates, this.listSelector); + const elementInfo = await getElementInformation(page, coordinates, this.listSelector, this.getList); const selectorBasedOnCustomAction = (this.getList === true) ? await getNonUniqueSelectors(page, coordinates) : await getSelectors(page, coordinates); @@ -569,9 +569,9 @@ export class WorkflowGenerator { * @returns {Promise} */ public generateDataForHighlighter = async (page: Page, coordinates: Coordinates) => { - const rect = await getRect(page, coordinates, this.listSelector); + const rect = await getRect(page, coordinates, this.listSelector, this.getList); const displaySelector = await this.generateSelector(page, coordinates, ActionType.Click); - const elementInfo = await getElementInformation(page, coordinates, this.listSelector); + const elementInfo = await getElementInformation(page, coordinates, this.listSelector, this.getList); if (rect) { if (this.getList === true) { if (this.listSelector !== '') { diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index bde38300..fd25a617 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -17,6 +17,7 @@ export const getElementInformation = async ( page: Page, coordinates: Coordinates, listSelector: string, + getList: boolean ) => { try { if (listSelector !== '') { @@ -155,7 +156,7 @@ export const getElementInformation = async ( * @category WorkflowManagement-Selectors * @returns {Promise} */ -export const getRect = async (page: Page, coordinates: Coordinates, listSelector: string) => { +export const getRect = async (page: Page, coordinates: Coordinates, listSelector: string, getList: boolean) => { try { if (listSelector !== '') { const rect = await page.evaluate( From ddb880df668e84ebce1d7731f6dda6aa2413a486 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Sun, 15 Dec 2024 05:07:45 +0530 Subject: [PATCH 44/88] fix: capture text selection --- 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 fd25a617..36b592a6 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -20,7 +20,7 @@ export const getElementInformation = async ( getList: boolean ) => { try { - if (listSelector !== '') { + if (!getList) { const elementInfo = await page.evaluate( async ({ x, y }) => { const el = document.elementFromPoint(x, y) as HTMLElement; @@ -158,7 +158,7 @@ export const getElementInformation = async ( */ export const getRect = async (page: Page, coordinates: Coordinates, listSelector: string, getList: boolean) => { try { - if (listSelector !== '') { + if (!getList) { const rect = await page.evaluate( async ({ x, y }) => { const el = document.elementFromPoint(x, y) as HTMLElement; From 97e7c89105132d42864be52386e9971208f88d8f Mon Sep 17 00:00:00 2001 From: amhsirak Date: Sun, 15 Dec 2024 05:13:08 +0530 Subject: [PATCH 45/88] feat: re-add listSelector empty check for child selection --- 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 36b592a6..699cb669 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -20,7 +20,7 @@ export const getElementInformation = async ( getList: boolean ) => { try { - if (!getList) { + if (!getList || (getList && listSelector !== '')) { const elementInfo = await page.evaluate( async ({ x, y }) => { const el = document.elementFromPoint(x, y) as HTMLElement; @@ -158,7 +158,7 @@ export const getElementInformation = async ( */ export const getRect = async (page: Page, coordinates: Coordinates, listSelector: string, getList: boolean) => { try { - if (!getList) { + if (!getList || (getList && listSelector !== '')) { const rect = await page.evaluate( async ({ x, y }) => { const el = document.elementFromPoint(x, y) as HTMLElement; From 0c3b1e3e53c4e52c7898a2f8637884c0f07e118a Mon Sep 17 00:00:00 2001 From: amhsirak Date: Sun, 15 Dec 2024 05:29:30 +0530 Subject: [PATCH 46/88] feat: paass listSelect --- .../workflow-management/classes/Generator.ts | 2 +- server/src/workflow-management/selector.ts | 40 ++++++++++--------- 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/server/src/workflow-management/classes/Generator.ts b/server/src/workflow-management/classes/Generator.ts index 2ab54753..31775261 100644 --- a/server/src/workflow-management/classes/Generator.ts +++ b/server/src/workflow-management/classes/Generator.ts @@ -543,7 +543,7 @@ export class WorkflowGenerator { private generateSelector = async (page: Page, coordinates: Coordinates, action: ActionType) => { const elementInfo = await getElementInformation(page, coordinates, this.listSelector, this.getList); const selectorBasedOnCustomAction = (this.getList === true) - ? await getNonUniqueSelectors(page, coordinates) + ? await getNonUniqueSelectors(page, coordinates, this.listSelector) : await getSelectors(page, coordinates); const bestSelector = getBestSelectorForAction( diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index 699cb669..527a800f 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -852,7 +852,7 @@ interface SelectorResult { * @returns {Promise} */ -export const getNonUniqueSelectors = async (page: Page, coordinates: Coordinates): Promise => { +export const getNonUniqueSelectors = async (page: Page, coordinates: Coordinates, listSelector: string): Promise => { try { const selectors = await page.evaluate(({ x, y }: { x: number, y: number }) => { function getNonUniqueSelector(element: HTMLElement): string { @@ -891,24 +891,26 @@ export const getNonUniqueSelectors = async (page: Page, coordinates: Coordinates let element = originalEl; - while (element.parentElement) { - const parentRect = element.parentElement.getBoundingClientRect(); - const childRect = element.getBoundingClientRect(); - - const fullyContained = - parentRect.left <= childRect.left && - parentRect.right >= childRect.right && - parentRect.top <= childRect.top && - parentRect.bottom >= childRect.bottom; - - const significantOverlap = - (childRect.width * childRect.height) / - (parentRect.width * parentRect.height) > 0.5; - - if (fullyContained && significantOverlap) { - element = element.parentElement; - } else { - break; + if (listSelector === '') { + while (element.parentElement) { + const parentRect = element.parentElement.getBoundingClientRect(); + const childRect = element.getBoundingClientRect(); + + const fullyContained = + parentRect.left <= childRect.left && + parentRect.right >= childRect.right && + parentRect.top <= childRect.top && + parentRect.bottom >= childRect.bottom; + + const significantOverlap = + (childRect.width * childRect.height) / + (parentRect.width * parentRect.height) > 0.5; + + if (fullyContained && significantOverlap) { + element = element.parentElement; + } else { + break; + } } } From e1476935db354b7030eced9447c280241defb713 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Mon, 16 Dec 2024 07:25:13 +0530 Subject: [PATCH 47/88] fix: dont pass listSelector to non unique --- server/src/workflow-management/classes/Generator.ts | 2 +- server/src/workflow-management/selector.ts | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/server/src/workflow-management/classes/Generator.ts b/server/src/workflow-management/classes/Generator.ts index 31775261..2ab54753 100644 --- a/server/src/workflow-management/classes/Generator.ts +++ b/server/src/workflow-management/classes/Generator.ts @@ -543,7 +543,7 @@ export class WorkflowGenerator { private generateSelector = async (page: Page, coordinates: Coordinates, action: ActionType) => { const elementInfo = await getElementInformation(page, coordinates, this.listSelector, this.getList); const selectorBasedOnCustomAction = (this.getList === true) - ? await getNonUniqueSelectors(page, coordinates, this.listSelector) + ? await getNonUniqueSelectors(page, coordinates) : await getSelectors(page, coordinates); const bestSelector = getBestSelectorForAction( diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index 527a800f..070a897d 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -20,7 +20,8 @@ export const getElementInformation = async ( getList: boolean ) => { try { - if (!getList || (getList && listSelector !== '')) { + console.log(`List Selector Value From EL INFO: ->> ${listSelector !== '' ? listSelector: 'It is empty'}`); + if (!getList ||listSelector !== '') { const elementInfo = await page.evaluate( async ({ x, y }) => { const el = document.elementFromPoint(x, y) as HTMLElement; @@ -158,7 +159,7 @@ export const getElementInformation = async ( */ export const getRect = async (page: Page, coordinates: Coordinates, listSelector: string, getList: boolean) => { try { - if (!getList || (getList && listSelector !== '')) { + if (!getList || listSelector !== '') { const rect = await page.evaluate( async ({ x, y }) => { const el = document.elementFromPoint(x, y) as HTMLElement; @@ -852,7 +853,7 @@ interface SelectorResult { * @returns {Promise} */ -export const getNonUniqueSelectors = async (page: Page, coordinates: Coordinates, listSelector: string): Promise => { +export const getNonUniqueSelectors = async (page: Page, coordinates: Coordinates): Promise => { try { const selectors = await page.evaluate(({ x, y }: { x: number, y: number }) => { function getNonUniqueSelector(element: HTMLElement): string { @@ -891,7 +892,7 @@ export const getNonUniqueSelectors = async (page: Page, coordinates: Coordinates let element = originalEl; - if (listSelector === '') { + // if (listSelector === '') { while (element.parentElement) { const parentRect = element.parentElement.getBoundingClientRect(); const childRect = element.getBoundingClientRect(); @@ -912,7 +913,7 @@ export const getNonUniqueSelectors = async (page: Page, coordinates: Coordinates break; } } - } + // } const generalSelector = getSelectorPath(element); return { From 4a9496053177663e4081850cf9db57742899a578 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Mon, 16 Dec 2024 08:22:21 +0530 Subject: [PATCH 48/88] feat: push parentSelector --- 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 070a897d..e326f21b 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -968,6 +968,7 @@ export const getChildSelectors = async (page: Page, parentSelector: string): Pro const childPath = getSelectorPath(child); if (childPath) { selectors.push(childPath); // Add direct child path + selectors.push(parentSelector) selectors = selectors.concat(getAllDescendantSelectors(child)); // Recursively process descendants } } From 23ac1340840a33d307f3730a1ecf0485b06a80b8 Mon Sep 17 00:00:00 2001 From: RohitR311 Date: Mon, 16 Dec 2024 15:55:55 +0530 Subject: [PATCH 49/88] fix: add pair to workflow before decision socket emission --- .../workflow-management/classes/Generator.ts | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/server/src/workflow-management/classes/Generator.ts b/server/src/workflow-management/classes/Generator.ts index 57be015e..9fab04d3 100644 --- a/server/src/workflow-management/classes/Generator.ts +++ b/server/src/workflow-management/classes/Generator.ts @@ -140,19 +140,22 @@ export class WorkflowGenerator { socket.on('decision', async ({ pair, actionType, decision }) => { const id = browserPool.getActiveBrowserId(); if (id) { - const activeBrowser = browserPool.getRemoteBrowser(id); - const currentPage = activeBrowser?.getCurrentPage(); - if (decision) { + // const activeBrowser = browserPool.getRemoteBrowser(id); + // const currentPage = activeBrowser?.getCurrentPage(); + if (!decision) { switch (actionType) { case 'customAction': - pair.where.selectors = [this.generatedData.lastUsedSelector]; + // pair.where.selectors = [this.generatedData.lastUsedSelector]; + pair.where.selectors = pair.where.selectors.filter( + (selector: string) => selector !== this.generatedData.lastUsedSelector + ); break; default: break; } } - if (currentPage) { - await this.addPairToWorkflowAndNotifyClient(pair, currentPage); - } + // if (currentPage) { + // await this.addPairToWorkflowAndNotifyClient(pair, currentPage); + // } } }) socket.on('updatePair', (data) => { @@ -360,6 +363,8 @@ export class WorkflowGenerator { }], } + await this.addPairToWorkflowAndNotifyClient(pair, page); + if (this.generatedData.lastUsedSelector) { const elementInfo = await this.getLastUsedSelectorInfo(page, this.generatedData.lastUsedSelector); @@ -372,9 +377,7 @@ export class WorkflowGenerator { innerText: elementInfo.innerText, } }); - } else { - await this.addPairToWorkflowAndNotifyClient(pair, page); - } + } }; /** From 94df79404011d99cc6cc5d80bb1c77208f9abcc5 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Tue, 17 Dec 2024 02:48:14 +0530 Subject: [PATCH 50/88] feat: conditionally compute non unique --- server/src/workflow-management/selector.ts | 55 ++++++++++++++++++++-- 1 file changed, 51 insertions(+), 4 deletions(-) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index e326f21b..60f5bdbd 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -20,8 +20,7 @@ export const getElementInformation = async ( getList: boolean ) => { try { - console.log(`List Selector Value From EL INFO: ->> ${listSelector !== '' ? listSelector: 'It is empty'}`); - if (!getList ||listSelector !== '') { + if (!getList || listSelector !== '') { const elementInfo = await page.evaluate( async ({ x, y }) => { const el = document.elementFromPoint(x, y) as HTMLElement; @@ -853,8 +852,10 @@ interface SelectorResult { * @returns {Promise} */ -export const getNonUniqueSelectors = async (page: Page, coordinates: Coordinates): Promise => { +export const getNonUniqueSelectors = async (page: Page, coordinates: Coordinates, listSelector: string): Promise => { try { + if (!listSelector) { + console.log(`NON UNIQUE: MODE 1`) const selectors = await page.evaluate(({ x, y }: { x: number, y: number }) => { function getNonUniqueSelector(element: HTMLElement): string { let selector = element.tagName.toLowerCase(); @@ -920,8 +921,54 @@ export const getNonUniqueSelectors = async (page: Page, coordinates: Coordinates generalSelector, }; }, coordinates); - return selectors || { generalSelector: '' }; + } else { + console.log(`NON UNIQUE: MODE 2`) + const selectors = await page.evaluate(({ x, y }: { x: number, y: number }) => { + function getNonUniqueSelector(element: HTMLElement): string { + let selector = element.tagName.toLowerCase(); + + if (element.className) { + const classes = element.className.split(/\s+/).filter((cls: string) => Boolean(cls)); + if (classes.length > 0) { + const validClasses = classes.filter((cls: string) => !cls.startsWith('!') && !cls.includes(':')); + if (validClasses.length > 0) { + selector += '.' + validClasses.map(cls => CSS.escape(cls)).join('.'); + } + } + } + + return selector; + } + + function getSelectorPath(element: HTMLElement | null): string { + const path: string[] = []; + let depth = 0; + const maxDepth = 2; + + while (element && element !== document.body && depth < maxDepth) { + const selector = getNonUniqueSelector(element); + path.unshift(selector); + element = element.parentElement; + depth++; + } + + return path.join(' > '); + } + + const originalEl = document.elementFromPoint(x, y) as HTMLElement; + if (!originalEl) return null; + + let element = originalEl; + + const generalSelector = getSelectorPath(element); + return { + generalSelector, + }; + }, coordinates); + return selectors || { generalSelector: '' }; + } + } catch (error) { console.error('Error in getNonUniqueSelectors:', error); return { generalSelector: '' }; From 52b767188eedd3ef3c3053a3e50d054fb9b35e44 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Tue, 17 Dec 2024 02:48:38 +0530 Subject: [PATCH 51/88] feat: !push parentSelector --- server/src/workflow-management/selector.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index 60f5bdbd..9c62139b 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -1015,7 +1015,6 @@ export const getChildSelectors = async (page: Page, parentSelector: string): Pro const childPath = getSelectorPath(child); if (childPath) { selectors.push(childPath); // Add direct child path - selectors.push(parentSelector) selectors = selectors.concat(getAllDescendantSelectors(child)); // Recursively process descendants } } From 647cd62e32fba8fc55084dbacfef6029f071d076 Mon Sep 17 00:00:00 2001 From: RohitR311 Date: Tue, 17 Dec 2024 11:37:05 +0530 Subject: [PATCH 52/88] feat: add listSelector param --- server/src/workflow-management/classes/Generator.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/workflow-management/classes/Generator.ts b/server/src/workflow-management/classes/Generator.ts index 2ab54753..31775261 100644 --- a/server/src/workflow-management/classes/Generator.ts +++ b/server/src/workflow-management/classes/Generator.ts @@ -543,7 +543,7 @@ export class WorkflowGenerator { private generateSelector = async (page: Page, coordinates: Coordinates, action: ActionType) => { const elementInfo = await getElementInformation(page, coordinates, this.listSelector, this.getList); const selectorBasedOnCustomAction = (this.getList === true) - ? await getNonUniqueSelectors(page, coordinates) + ? await getNonUniqueSelectors(page, coordinates, this.listSelector) : await getSelectors(page, coordinates); const bestSelector = getBestSelectorForAction( From a9dc4c8f4ceeca8abe45268331b1369d4a1cbbb9 Mon Sep 17 00:00:00 2001 From: RohitR311 Date: Tue, 17 Dec 2024 12:21:56 +0530 Subject: [PATCH 53/88] feat: add conditional check to collect matching classes --- maxun-core/src/browserSide/scraper.js | 93 ++++++++++++++++++--------- 1 file changed, 62 insertions(+), 31 deletions(-) diff --git a/maxun-core/src/browserSide/scraper.js b/maxun-core/src/browserSide/scraper.js index 09b6578b..a2009d78 100644 --- a/maxun-core/src/browserSide/scraper.js +++ b/maxun-core/src/browserSide/scraper.js @@ -265,41 +265,72 @@ function scrapableHeuristics(maxCountPerPage = 50, minArea = 20000, scrolls = 3, const scrapedData = []; while (scrapedData.length < limit) { - // Get all parent elements matching the listSelector - const parentElements = Array.from(document.querySelectorAll(listSelector)); + let parentElements = Array.from(document.querySelectorAll(listSelector)); + + // If we only got one element or none, try a more generic approach + if (limit > 1 && parentElements.length <= 1) { + const [containerSelector, _] = listSelector.split('>').map(s => s.trim()); + const container = document.querySelector(containerSelector); + + if (container) { + const allChildren = Array.from(container.children); + + const firstMatch = document.querySelector(listSelector); + if (firstMatch) { + // Get classes from the first matching element + const firstMatchClasses = Array.from(firstMatch.classList); + + // Find similar elements by matching most of their classes + parentElements = allChildren.filter(element => { + const elementClasses = Array.from(element.classList); - // Iterate through each parent element - for (const parent of parentElements) { - if (scrapedData.length >= limit) break; - const record = {}; - - // For each field, select the corresponding element within the parent - for (const [label, { selector, attribute }] of Object.entries(fields)) { - const fieldElement = parent.querySelector(selector); - - if (fieldElement) { - if (attribute === 'innerText') { - record[label] = fieldElement.innerText.trim(); - } else if (attribute === 'innerHTML') { - record[label] = fieldElement.innerHTML.trim(); - } else if (attribute === 'src') { - // Handle relative 'src' URLs - const src = fieldElement.getAttribute('src'); - record[label] = src ? new URL(src, window.location.origin).href : null; - } else if (attribute === 'href') { - // Handle relative 'href' URLs - const href = fieldElement.getAttribute('href'); - record[label] = href ? new URL(href, window.location.origin).href : null; - } else { - record[label] = fieldElement.getAttribute(attribute); + // Element should share at least 70% of classes with the first match + const commonClasses = firstMatchClasses.filter(cls => + elementClasses.includes(cls)); + return commonClasses.length >= Math.floor(firstMatchClasses.length * 0.7); + }); + } } - } } - scrapedData.push(record); - } + + // Iterate through each parent element + for (const parent of parentElements) { + if (scrapedData.length >= limit) break; + const record = {}; + + // For each field, select the corresponding element within the parent + for (const [label, { selector, attribute }] of Object.entries(fields)) { + const fieldElement = parent.querySelector(selector); + + if (fieldElement) { + if (attribute === 'innerText') { + record[label] = fieldElement.innerText.trim(); + } else if (attribute === 'innerHTML') { + record[label] = fieldElement.innerHTML.trim(); + } else if (attribute === 'src') { + // Handle relative 'src' URLs + const src = fieldElement.getAttribute('src'); + record[label] = src ? new URL(src, window.location.origin).href : null; + } else if (attribute === 'href') { + // Handle relative 'href' URLs + const href = fieldElement.getAttribute('href'); + record[label] = href ? new URL(href, window.location.origin).href : null; + } else { + record[label] = fieldElement.getAttribute(attribute); + } + } + } + scrapedData.push(record); + } + + // If we've processed all available elements and still haven't reached the limit, + // break to avoid infinite loop + if (parentElements.length === 0 || scrapedData.length >= parentElements.length) { + break; + } } - return scrapedData - }; + return scrapedData; +}; /** From e34cfda770d9ff8c0c17cb6418c085ade066e7ec Mon Sep 17 00:00:00 2001 From: RohitR311 Date: Tue, 17 Dec 2024 21:55:24 +0530 Subject: [PATCH 54/88] fix: skip click action if selector not visible --- maxun-core/src/interpret.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/maxun-core/src/interpret.ts b/maxun-core/src/interpret.ts index d1cc8318..ef06d1ab 100644 --- a/maxun-core/src/interpret.ts +++ b/maxun-core/src/interpret.ts @@ -506,7 +506,11 @@ export default class Interpreter extends EventEmitter { try { await executeAction(invokee, methodName, step.args); } catch (error) { - await executeAction(invokee, methodName, [step.args[0], { force: true }]); + try{ + await executeAction(invokee, methodName, [step.args[0], { force: true }]); + } catch (error) { + continue + } } } else { await executeAction(invokee, methodName, step.args); From 0783cbc1c5679c88ea6973a4a0dd831dcae7b8ba Mon Sep 17 00:00:00 2001 From: RohitR311 Date: Wed, 18 Dec 2024 18:17:50 +0530 Subject: [PATCH 55/88] feat: add date selection handler --- .../workflow-management/classes/Generator.ts | 37 ++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/server/src/workflow-management/classes/Generator.ts b/server/src/workflow-management/classes/Generator.ts index 213a0e86..06eac494 100644 --- a/server/src/workflow-management/classes/Generator.ts +++ b/server/src/workflow-management/classes/Generator.ts @@ -1,4 +1,4 @@ -import { Action, ActionType, Coordinates, TagName } from "../../types"; +import { Action, ActionType, Coordinates, TagName, DatePickerEventData } from "../../types"; import { WhereWhatPair, WorkflowFile } from 'maxun-core'; import logger from "../../logger"; import { Socket } from "socket.io"; @@ -255,6 +255,25 @@ export class WorkflowGenerator { logger.log('info', `Workflow emitted`); }; + public onDateSelection = async (page: Page, data: DatePickerEventData) => { + const { selector, value } = data; + + try { + await page.fill(selector, value); + } catch (error) { + console.error("Failed to fill date value:", error); + } + + const pair: WhereWhatPair = { + where: { url: this.getBestUrl(page.url()) }, + what: [{ + action: 'fill', + args: [selector, value], + }], + }; + + await this.addPairToWorkflowAndNotifyClient(pair, page); + }; /** * Generates a pair for the click event. @@ -266,6 +285,22 @@ export class WorkflowGenerator { let where: WhereWhatPair["where"] = { url: this.getBestUrl(page.url()) }; const selector = await this.generateSelector(page, coordinates, ActionType.Click); logger.log('debug', `Element's selector: ${selector}`); + + // Check if clicked element is a date input + const isDateInput = await page.evaluate(({x, y}) => { + const element = document.elementFromPoint(x, y); + return element instanceof HTMLInputElement && element.type === 'date'; + }, coordinates); + + if (isDateInput) { + // Notify client to show datepicker overlay + this.socket.emit('showDatePicker', { + coordinates, + selector + }); + return; + } + //const element = await getElementMouseIsOver(page, coordinates); //logger.log('debug', `Element: ${JSON.stringify(element, null, 2)}`); if (selector) { From 7ac79dc31c7ffa26d94d4b58f9d50088e238fca7 Mon Sep 17 00:00:00 2001 From: RohitR311 Date: Wed, 18 Dec 2024 18:19:05 +0530 Subject: [PATCH 56/88] feat: add date selection event handlers --- .../src/browser-management/inputHandlers.ts | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/server/src/browser-management/inputHandlers.ts b/server/src/browser-management/inputHandlers.ts index d6902b3f..2e722e9d 100644 --- a/server/src/browser-management/inputHandlers.ts +++ b/server/src/browser-management/inputHandlers.ts @@ -6,7 +6,7 @@ import { Socket } from 'socket.io'; import logger from "../logger"; -import { Coordinates, ScrollDeltas, KeyboardInput } from '../types'; +import { Coordinates, ScrollDeltas, KeyboardInput, DatePickerEventData } from '../types'; import { browserPool } from "../server"; import { WorkflowGenerator } from "../workflow-management/classes/Generator"; import { Page } from "playwright"; @@ -223,6 +223,23 @@ const handleKeydown = async (generator: WorkflowGenerator, page: Page, { key, co logger.log('debug', `Key ${key} pressed`); }; +/** + * Handles the date selection event. + * @param generator - the workflow generator {@link Generator} + * @param page - the active page of the remote browser + * @param data - the data of the date selection event {@link DatePickerEventData} + * @category BrowserManagement + */ +const handleDateSelection = async (generator: WorkflowGenerator, page: Page, data: DatePickerEventData) => { + await generator.onDateSelection(page, data); + logger.log('debug', `Date ${data.value} selected`); +} + +const onDateSelection = async (data: DatePickerEventData) => { + logger.log('debug', 'Handling date selection event emitted from client'); + await handleWrapper(handleDateSelection, data); +} + /** * A wrapper function for handling the keyup event. * @param keyboardInput - the keyboard input of the keyup event @@ -378,6 +395,7 @@ const registerInputHandlers = (socket: Socket) => { socket.on("input:refresh", onRefresh); socket.on("input:back", onGoBack); socket.on("input:forward", onGoForward); + socket.on("input:date", onDateSelection); socket.on("action", onGenerateAction); }; From 7eea077e70124cc9a24faaa768d7752305158685 Mon Sep 17 00:00:00 2001 From: RohitR311 Date: Wed, 18 Dec 2024 18:21:05 +0530 Subject: [PATCH 57/88] feat: add interface to hanle date picker event data --- server/src/types/index.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/server/src/types/index.ts b/server/src/types/index.ts index 4fe761f1..f2e327ef 100644 --- a/server/src/types/index.ts +++ b/server/src/types/index.ts @@ -20,6 +20,16 @@ export interface Coordinates { y: number; } +/** + * interface to handle date picker events. + * @category Types + */ +export interface DatePickerEventData { + coordinates: Coordinates; + selector: string; + value: string; +} + /** * Holds the deltas of a wheel/scroll event. * @category Types From ec4d1acfa27c9c0ff5ef1fa6ae5415ecad6ebf15 Mon Sep 17 00:00:00 2001 From: RohitR311 Date: Wed, 18 Dec 2024 18:22:51 +0530 Subject: [PATCH 58/88] feat: trigger socket event to display date picker --- src/components/atoms/canvas.tsx | 35 +++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/components/atoms/canvas.tsx b/src/components/atoms/canvas.tsx index 1dd88e19..84d6a620 100644 --- a/src/components/atoms/canvas.tsx +++ b/src/components/atoms/canvas.tsx @@ -3,6 +3,7 @@ import { useSocketStore } from '../../context/socket'; import { getMappedCoordinates } from "../../helpers/inputHelpers"; import { useGlobalInfoStore } from "../../context/globalInfo"; import { useActionContext } from '../../context/browserActions'; +import DatePicker from './DatePicker'; interface CreateRefCallback { (ref: React.RefObject): void; @@ -31,6 +32,11 @@ const Canvas = ({ width, height, onCreateRef }: CanvasProps) => { const getTextRef = useRef(getText); const getListRef = useRef(getList); + const [datePickerInfo, setDatePickerInfo] = React.useState<{ + coordinates: Coordinates; + selector: string; + } | null>(null); + const notifyLastAction = (action: string) => { if (lastAction !== action) { setLastAction(action); @@ -44,6 +50,28 @@ const Canvas = ({ width, height, onCreateRef }: CanvasProps) => { getListRef.current = getList; }, [getText, getList]); + useEffect(() => { + if (socket) { + socket.on('showDatePicker', (info: {coordinates: Coordinates, selector: string}) => { + setDatePickerInfo(info); + }); + + return () => { + socket.off('showDatePicker'); + }; + } + }, [socket]); + + const handleDateSelect = (value: string) => { + if (socket && datePickerInfo) { + socket.emit('input:date', { + selector: datePickerInfo.selector, + value + }); + setDatePickerInfo(null); + } + }; + const onMouseEvent = useCallback((event: MouseEvent) => { if (socket && canvasRef.current) { // Get the canvas bounding rectangle @@ -146,6 +174,13 @@ const Canvas = ({ width, height, onCreateRef }: CanvasProps) => { width={900} style={{ display: 'block' }} /> + {datePickerInfo && ( + setDatePickerInfo(null)} + /> + )} ); From 9ee54d118b522d87c7e37791e69f3a6496f53489 Mon Sep 17 00:00:00 2001 From: RohitR311 Date: Wed, 18 Dec 2024 18:23:56 +0530 Subject: [PATCH 59/88] feat: rm onHandleSelct callback function --- src/components/atoms/canvas.tsx | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/components/atoms/canvas.tsx b/src/components/atoms/canvas.tsx index 84d6a620..13966abd 100644 --- a/src/components/atoms/canvas.tsx +++ b/src/components/atoms/canvas.tsx @@ -62,16 +62,6 @@ const Canvas = ({ width, height, onCreateRef }: CanvasProps) => { } }, [socket]); - const handleDateSelect = (value: string) => { - if (socket && datePickerInfo) { - socket.emit('input:date', { - selector: datePickerInfo.selector, - value - }); - setDatePickerInfo(null); - } - }; - const onMouseEvent = useCallback((event: MouseEvent) => { if (socket && canvasRef.current) { // Get the canvas bounding rectangle From 2e301924226fda12ce8fea82828b3eaa4c2976c6 Mon Sep 17 00:00:00 2001 From: RohitR311 Date: Wed, 18 Dec 2024 18:24:51 +0530 Subject: [PATCH 60/88] feat: add date picker component to input date --- src/components/atoms/DatePicker.tsx | 74 +++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 src/components/atoms/DatePicker.tsx diff --git a/src/components/atoms/DatePicker.tsx b/src/components/atoms/DatePicker.tsx new file mode 100644 index 00000000..30d3b869 --- /dev/null +++ b/src/components/atoms/DatePicker.tsx @@ -0,0 +1,74 @@ +import React, { useState } from 'react'; +import { useSocketStore } from '../../context/socket'; +import { Coordinates } from './canvas'; + +interface DatePickerProps { + coordinates: Coordinates; + selector: string; + onClose: () => void; +} + +const DatePicker: React.FC = ({ coordinates, selector, onClose }) => { + const { socket } = useSocketStore(); + const [selectedDate, setSelectedDate] = useState(''); + + const handleDateChange = (e: React.ChangeEvent) => { + setSelectedDate(e.target.value); + }; + + const handleConfirm = () => { + if (socket && selectedDate) { + socket.emit('input:date', { + selector, + value: selectedDate + }); + onClose(); + } + }; + + return ( +
+
+ +
+ + +
+
+
+ ); +}; + +export default DatePicker; \ No newline at end of file From 14079fa0f89029502e6c3671aab0d9232e17e5a9 Mon Sep 17 00:00:00 2001 From: RohitR311 Date: Thu, 19 Dec 2024 12:13:17 +0530 Subject: [PATCH 61/88] feat: date input check using element information --- server/src/workflow-management/classes/Generator.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/server/src/workflow-management/classes/Generator.ts b/server/src/workflow-management/classes/Generator.ts index 06eac494..96645de0 100644 --- a/server/src/workflow-management/classes/Generator.ts +++ b/server/src/workflow-management/classes/Generator.ts @@ -286,11 +286,11 @@ export class WorkflowGenerator { const selector = await this.generateSelector(page, coordinates, ActionType.Click); logger.log('debug', `Element's selector: ${selector}`); + const elementInfo = await getElementInformation(page, coordinates, '', false); + console.log("Element info: ", elementInfo); + // Check if clicked element is a date input - const isDateInput = await page.evaluate(({x, y}) => { - const element = document.elementFromPoint(x, y); - return element instanceof HTMLInputElement && element.type === 'date'; - }, coordinates); + const isDateInput = elementInfo?.tagName === 'INPUT' && elementInfo?.attributes?.type === 'date'; if (isDateInput) { // Notify client to show datepicker overlay From a6ed8c67b8b19e7d684e89f4e2985947a48d4522 Mon Sep 17 00:00:00 2001 From: RohitR311 Date: Thu, 19 Dec 2024 12:14:08 +0530 Subject: [PATCH 62/88] feat: check for select type and emit dropdown socket event --- .../workflow-management/classes/Generator.ts | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/server/src/workflow-management/classes/Generator.ts b/server/src/workflow-management/classes/Generator.ts index 96645de0..a221a47b 100644 --- a/server/src/workflow-management/classes/Generator.ts +++ b/server/src/workflow-management/classes/Generator.ts @@ -289,6 +289,45 @@ export class WorkflowGenerator { const elementInfo = await getElementInformation(page, coordinates, '', false); console.log("Element info: ", elementInfo); + // Check if clicked element is a select dropdown + const isDropdown = elementInfo?.tagName === 'SELECT'; + + if (isDropdown && elementInfo.innerHTML) { + // Parse options from innerHTML + const options = elementInfo.innerHTML + .split(' { + const valueMatch = optionHtml.match(/value="([^"]*)"/); + const disabledMatch = optionHtml.includes('disabled="disabled"'); + const selectedMatch = optionHtml.includes('selected="selected"'); + + // Extract text content between > and + const textMatch = optionHtml.match(/>([^<]*) Date: Thu, 19 Dec 2024 12:23:13 +0530 Subject: [PATCH 63/88] feat: add dropdown selection action pair to workflow --- .../workflow-management/classes/Generator.ts | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/server/src/workflow-management/classes/Generator.ts b/server/src/workflow-management/classes/Generator.ts index a221a47b..3395e9a1 100644 --- a/server/src/workflow-management/classes/Generator.ts +++ b/server/src/workflow-management/classes/Generator.ts @@ -275,6 +275,26 @@ export class WorkflowGenerator { await this.addPairToWorkflowAndNotifyClient(pair, page); }; + public onDropdownSelection = async (page: Page, data: { selector: string, value: string }) => { + const { selector, value } = data; + + try { + await page.selectOption(selector, value); + } catch (error) { + console.error("Failed to fill date value:", error); + } + + const pair: WhereWhatPair = { + where: { url: this.getBestUrl(page.url()) }, + what: [{ + action: 'selectOption', + args: [selector, value], + }], + }; + + await this.addPairToWorkflowAndNotifyClient(pair, page); + }; + /** * Generates a pair for the click event. * @param coordinates The coordinates of the click event. From d8b5ae4113d5a4201680e4e23ff6721708ec0199 Mon Sep 17 00:00:00 2001 From: RohitR311 Date: Thu, 19 Dec 2024 12:24:19 +0530 Subject: [PATCH 64/88] feat: add dropdown selection handler functions and register socket event --- server/src/browser-management/inputHandlers.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/server/src/browser-management/inputHandlers.ts b/server/src/browser-management/inputHandlers.ts index 2e722e9d..4b37480f 100644 --- a/server/src/browser-management/inputHandlers.ts +++ b/server/src/browser-management/inputHandlers.ts @@ -240,6 +240,16 @@ const onDateSelection = async (data: DatePickerEventData) => { await handleWrapper(handleDateSelection, data); } +const handleDropdownSelection = async (generator: WorkflowGenerator, page: Page, data: { selector: string, value: string }) => { + await generator.onDropdownSelection(page, data); + logger.log('debug', `Dropdown value ${data.value} selected`); +} + +const onDropdownSelection = async (data: { selector: string, value: string }) => { + logger.log('debug', 'Handling dropdown selection event emitted from client'); + await handleWrapper(handleDropdownSelection, data); +} + /** * A wrapper function for handling the keyup event. * @param keyboardInput - the keyboard input of the keyup event @@ -396,6 +406,7 @@ const registerInputHandlers = (socket: Socket) => { socket.on("input:back", onGoBack); socket.on("input:forward", onGoForward); socket.on("input:date", onDateSelection); + socket.on("input:dropdown", onDropdownSelection); socket.on("action", onGenerateAction); }; From 7bd7a84173d47be16a0f828a1067e7fca7a2ed8d Mon Sep 17 00:00:00 2001 From: RohitR311 Date: Thu, 19 Dec 2024 14:04:05 +0530 Subject: [PATCH 65/88] feat: tigger socket event to display dropdown --- src/components/atoms/canvas.tsx | 34 +++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/src/components/atoms/canvas.tsx b/src/components/atoms/canvas.tsx index 13966abd..fc778963 100644 --- a/src/components/atoms/canvas.tsx +++ b/src/components/atoms/canvas.tsx @@ -4,6 +4,7 @@ import { getMappedCoordinates } from "../../helpers/inputHelpers"; import { useGlobalInfoStore } from "../../context/globalInfo"; import { useActionContext } from '../../context/browserActions'; import DatePicker from './DatePicker'; +import Dropdown from './Dropdown'; interface CreateRefCallback { (ref: React.RefObject): void; @@ -37,6 +38,17 @@ const Canvas = ({ width, height, onCreateRef }: CanvasProps) => { selector: string; } | null>(null); + const [dropdownInfo, setDropdownInfo] = React.useState<{ + coordinates: Coordinates; + selector: string; + options: Array<{ + value: string; + text: string; + disabled: boolean; + selected: boolean; + }>; + } | null>(null); + const notifyLastAction = (action: string) => { if (lastAction !== action) { setLastAction(action); @@ -56,8 +68,22 @@ const Canvas = ({ width, height, onCreateRef }: CanvasProps) => { setDatePickerInfo(info); }); + socket.on('showDropdown', (info: { + coordinates: Coordinates, + selector: string, + options: Array<{ + value: string; + text: string; + disabled: boolean; + selected: boolean; + }>; + }) => { + setDropdownInfo(info); + }); + return () => { socket.off('showDatePicker'); + socket.off('showDropdown'); }; } }, [socket]); @@ -171,6 +197,14 @@ const Canvas = ({ width, height, onCreateRef }: CanvasProps) => { onClose={() => setDatePickerInfo(null)} /> )} + {dropdownInfo && ( + setDropdownInfo(null)} + /> + )} ); From 13b92ee5dce7d7e33e3e0f60c608e202187f01fb Mon Sep 17 00:00:00 2001 From: RohitR311 Date: Thu, 19 Dec 2024 14:05:02 +0530 Subject: [PATCH 66/88] feat: add dropdown component to input dropdown --- src/components/atoms/Dropdown.tsx | 85 +++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 src/components/atoms/Dropdown.tsx diff --git a/src/components/atoms/Dropdown.tsx b/src/components/atoms/Dropdown.tsx new file mode 100644 index 00000000..c7ead64b --- /dev/null +++ b/src/components/atoms/Dropdown.tsx @@ -0,0 +1,85 @@ +import React, { useState } from 'react'; +import { useSocketStore } from '../../context/socket'; +import { Coordinates } from './canvas'; + +interface DropdownProps { + coordinates: Coordinates; + selector: string; + options: Array<{ + value: string; + text: string; + disabled: boolean; + selected: boolean; + }>; + onClose: () => void; +} + +const Dropdown = ({ coordinates, selector, options, onClose }: DropdownProps) => { + const { socket } = useSocketStore(); + const [hoveredIndex, setHoveredIndex] = useState(null); + + const handleSelect = (value: string) => { + if (socket) { + socket.emit('input:dropdown', { selector, value }); + } + onClose(); + }; + + const containerStyle: React.CSSProperties = { + position: 'absolute', + left: coordinates.x, + top: coordinates.y, + zIndex: 1000, + width: '200px', + backgroundColor: 'white', + border: '1px solid rgb(169, 169, 169)', + boxShadow: '0 2px 4px rgba(0,0,0,0.15)', + }; + + const scrollContainerStyle: React.CSSProperties = { + maxHeight: '180px', + overflowY: 'auto', + overflowX: 'hidden', + }; + + const getOptionStyle = (option: any, index: number): React.CSSProperties => ({ + fontSize: '13.333px', + lineHeight: '18px', + padding: '0 3px', + cursor: option.disabled ? 'default' : 'default', + backgroundColor: hoveredIndex === index ? '#0078D7' : + option.selected ? '#0078D7' : + option.disabled ? '#f8f8f8' : 'white', + color: (hoveredIndex === index || option.selected) ? 'white' : + option.disabled ? '#a0a0a0' : 'black', + userSelect: 'none', + }); + + return ( +
+
e.stopPropagation()} + > +
+ {options.map((option, index) => ( +
!option.disabled && setHoveredIndex(index)} + onMouseLeave={() => setHoveredIndex(null)} + onClick={() => !option.disabled && handleSelect(option.value)} + > + {option.text} +
+ ))} +
+
+
+ ); +}; + +export default Dropdown; \ No newline at end of file From 947a6b75cb9d2d431d23ecfd98ec20cc1a27e855 Mon Sep 17 00:00:00 2001 From: RohitR311 Date: Thu, 19 Dec 2024 16:16:47 +0530 Subject: [PATCH 67/88] feat: check for time input field and emit socket event --- .../workflow-management/classes/Generator.ts | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/server/src/workflow-management/classes/Generator.ts b/server/src/workflow-management/classes/Generator.ts index 3395e9a1..9ff4922e 100644 --- a/server/src/workflow-management/classes/Generator.ts +++ b/server/src/workflow-management/classes/Generator.ts @@ -295,6 +295,26 @@ export class WorkflowGenerator { await this.addPairToWorkflowAndNotifyClient(pair, page); }; + public onTimeSelection = async (page: Page, data: { selector: string, value: string }) => { + const { selector, value } = data; + + try { + await page.fill(selector, value); + } catch (error) { + console.error("Failed to set time value:", error); + } + + const pair: WhereWhatPair = { + where: { url: this.getBestUrl(page.url()) }, + what: [{ + action: 'fill', + args: [selector, value], + }], + }; + + await this.addPairToWorkflowAndNotifyClient(pair, page); + }; + /** * Generates a pair for the click event. * @param coordinates The coordinates of the click event. @@ -360,6 +380,16 @@ export class WorkflowGenerator { return; } + const isTimeInput = elementInfo?.tagName === 'INPUT' && elementInfo?.attributes?.type === 'time'; + + if (isTimeInput) { + this.socket.emit('showTimePicker', { + coordinates, + selector + }); + return; + } + //const element = await getElementMouseIsOver(page, coordinates); //logger.log('debug', `Element: ${JSON.stringify(element, null, 2)}`); if (selector) { From 0b2d099dc0348af577762aa2588250e2c8c6202a Mon Sep 17 00:00:00 2001 From: RohitR311 Date: Thu, 19 Dec 2024 16:25:48 +0530 Subject: [PATCH 68/88] feat: add time selection event handlers --- server/src/browser-management/inputHandlers.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/server/src/browser-management/inputHandlers.ts b/server/src/browser-management/inputHandlers.ts index 4b37480f..982e18de 100644 --- a/server/src/browser-management/inputHandlers.ts +++ b/server/src/browser-management/inputHandlers.ts @@ -250,6 +250,16 @@ const onDropdownSelection = async (data: { selector: string, value: string }) => await handleWrapper(handleDropdownSelection, data); } +const handleTimeSelection = async (generator: WorkflowGenerator, page: Page, data: { selector: string, value: string }) => { + await generator.onTimeSelection(page, data); + logger.log('debug', `Time value ${data.value} selected`); +} + +const onTimeSelection = async (data: { selector: string, value: string }) => { + logger.log('debug', 'Handling time selection event emitted from client'); + await handleWrapper(handleTimeSelection, data); +} + /** * A wrapper function for handling the keyup event. * @param keyboardInput - the keyboard input of the keyup event @@ -407,6 +417,7 @@ const registerInputHandlers = (socket: Socket) => { socket.on("input:forward", onGoForward); socket.on("input:date", onDateSelection); socket.on("input:dropdown", onDropdownSelection); + socket.on("input:time", onTimeSelection); socket.on("action", onGenerateAction); }; From 66f3ccd34fa8c06da8dd35b5277e093b318109aa Mon Sep 17 00:00:00 2001 From: RohitR311 Date: Thu, 19 Dec 2024 16:26:23 +0530 Subject: [PATCH 69/88] trigger socket event to display time picker --- src/components/atoms/canvas.tsx | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/components/atoms/canvas.tsx b/src/components/atoms/canvas.tsx index fc778963..77128a65 100644 --- a/src/components/atoms/canvas.tsx +++ b/src/components/atoms/canvas.tsx @@ -5,6 +5,7 @@ import { useGlobalInfoStore } from "../../context/globalInfo"; import { useActionContext } from '../../context/browserActions'; import DatePicker from './DatePicker'; import Dropdown from './Dropdown'; +import TimePicker from './TimePicker'; interface CreateRefCallback { (ref: React.RefObject): void; @@ -49,6 +50,11 @@ const Canvas = ({ width, height, onCreateRef }: CanvasProps) => { }>; } | null>(null); + const [timePickerInfo, setTimePickerInfo] = React.useState<{ + coordinates: Coordinates; + selector: string; + } | null>(null); + const notifyLastAction = (action: string) => { if (lastAction !== action) { setLastAction(action); @@ -81,6 +87,10 @@ const Canvas = ({ width, height, onCreateRef }: CanvasProps) => { setDropdownInfo(info); }); + socket.on('showTimePicker', (info: {coordinates: Coordinates, selector: string}) => { + setTimePickerInfo(info); + }); + return () => { socket.off('showDatePicker'); socket.off('showDropdown'); @@ -205,6 +215,13 @@ const Canvas = ({ width, height, onCreateRef }: CanvasProps) => { onClose={() => setDropdownInfo(null)} /> )} + {timePickerInfo && ( + setTimePickerInfo(null)} + /> + )} ); From a97837d8b8cc20fcc943362d2745fff9d21c662d Mon Sep 17 00:00:00 2001 From: RohitR311 Date: Thu, 19 Dec 2024 16:26:57 +0530 Subject: [PATCH 70/88] feat: add time picker component to input time --- src/components/atoms/TimePicker.tsx | 130 ++++++++++++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 src/components/atoms/TimePicker.tsx diff --git a/src/components/atoms/TimePicker.tsx b/src/components/atoms/TimePicker.tsx new file mode 100644 index 00000000..31353c7a --- /dev/null +++ b/src/components/atoms/TimePicker.tsx @@ -0,0 +1,130 @@ +import React, { useState } from 'react'; +import { useSocketStore } from '../../context/socket'; +import { Coordinates } from './canvas'; + +interface TimePickerProps { + coordinates: Coordinates; + selector: string; + onClose: () => void; +} + +const TimePicker = ({ coordinates, selector, onClose }: TimePickerProps) => { + const { socket } = useSocketStore(); + const [hoveredHour, setHoveredHour] = useState(null); + const [hoveredMinute, setHoveredMinute] = useState(null); + const [selectedHour, setSelectedHour] = useState(null); + const [selectedMinute, setSelectedMinute] = useState(null); + + const handleHourSelect = (hour: number) => { + setSelectedHour(hour); + // If minute is already selected, complete the selection + if (selectedMinute !== null) { + const formattedHour = hour.toString().padStart(2, '0'); + const formattedMinute = selectedMinute.toString().padStart(2, '0'); + if (socket) { + socket.emit('input:time', { + selector, + value: `${formattedHour}:${formattedMinute}` + }); + } + onClose(); + } + }; + + const handleMinuteSelect = (minute: number) => { + setSelectedMinute(minute); + // If hour is already selected, complete the selection + if (selectedHour !== null) { + const formattedHour = selectedHour.toString().padStart(2, '0'); + const formattedMinute = minute.toString().padStart(2, '0'); + if (socket) { + socket.emit('input:time', { + selector, + value: `${formattedHour}:${formattedMinute}` + }); + } + onClose(); + } + }; + + const containerStyle: React.CSSProperties = { + position: 'absolute', + left: coordinates.x, + top: coordinates.y, + zIndex: 1000, + display: 'flex', + backgroundColor: 'white', + border: '1px solid rgb(169, 169, 169)', + boxShadow: '0 2px 4px rgba(0,0,0,0.15)', + }; + + const columnStyle: React.CSSProperties = { + width: '60px', + maxHeight: '180px', + overflowY: 'auto', + overflowX: 'hidden', + borderRight: '1px solid rgb(169, 169, 169)', + }; + + const getOptionStyle = (value: number, isHour: boolean): React.CSSProperties => { + const isHovered = isHour ? hoveredHour === value : hoveredMinute === value; + const isSelected = isHour ? selectedHour === value : selectedMinute === value; + + return { + fontSize: '13.333px', + lineHeight: '18px', + padding: '0 3px', + cursor: 'default', + backgroundColor: isSelected ? '#0078D7' : isHovered ? '#0078D7' : 'white', + color: (isSelected || isHovered) ? 'white' : 'black', + userSelect: 'none', + }; + }; + + const hours = Array.from({ length: 24 }, (_, i) => i); + const minutes = Array.from({ length: 60 }, (_, i) => i); + + return ( +
+
e.stopPropagation()} + > + {/* Hours column */} +
+ {hours.map((hour) => ( +
setHoveredHour(hour)} + onMouseLeave={() => setHoveredHour(null)} + onClick={() => handleHourSelect(hour)} + > + {hour.toString().padStart(2, '0')} +
+ ))} +
+ + {/* Minutes column */} +
+ {minutes.map((minute) => ( +
setHoveredMinute(minute)} + onMouseLeave={() => setHoveredMinute(null)} + onClick={() => handleMinuteSelect(minute)} + > + {minute.toString().padStart(2, '0')} +
+ ))} +
+
+
+ ); +}; + +export default TimePicker; \ No newline at end of file From 60b901a1a06c3240040dfefcbd9f861e506d71c8 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Fri, 20 Dec 2024 13:55:48 +0530 Subject: [PATCH 71/88] feat: handle select tags --- 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 9c62139b..e1dbf4db 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -53,7 +53,14 @@ export const getElementInformation = async ( info.url = (element as HTMLAnchorElement).href; info.innerText = element.innerText ?? ''; } else if (element?.tagName === 'IMG') { - info.imageUrl = (element as HTMLImageElement).src; + info.imageUrl = (element as HTMLImageElement).src; + } else if (element?.tagName === 'SELECT') { + const selectElement = element as HTMLSelectElement; + info.innerText = selectElement.options[selectElement.selectedIndex]?.text ?? ''; + info.attributes = { + ...info.attributes, + selectedValue: selectElement.value, + }; } else { info.hasOnlyText = element?.children?.length === 0 && element?.innerText?.length > 0; From 7860d404449f88b9c503cdd9b96433789bd167c9 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Fri, 20 Dec 2024 17:17:49 +0530 Subject: [PATCH 72/88] feat: handle input type time --- 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 e1dbf4db..0d17c9b4 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -61,6 +61,8 @@ export const getElementInformation = async ( ...info.attributes, selectedValue: selectElement.value, }; + } else if (element?.tagName === 'INPUT' && element?.type === 'time') { + info.innerText = element.value; } else { info.hasOnlyText = element?.children?.length === 0 && element?.innerText?.length > 0; From 4b2c0721392e1679a56e66c8b2e33111bc792e24 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Fri, 20 Dec 2024 17:18:06 +0530 Subject: [PATCH 73/88] feat: assign proper types --- 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 0d17c9b4..f1259abc 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -61,8 +61,8 @@ export const getElementInformation = async ( ...info.attributes, selectedValue: selectElement.value, }; - } else if (element?.tagName === 'INPUT' && element?.type === 'time') { - info.innerText = element.value; + } else if (element?.tagName === 'INPUT' && (element as HTMLInputElement).type === 'time') { + info.innerText = (element as HTMLInputElement).value; } else { info.hasOnlyText = element?.children?.length === 0 && element?.innerText?.length > 0; From 81eb32254c1cdea4f977f36bf362959d6ff611f1 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Fri, 20 Dec 2024 17:23:44 +0530 Subject: [PATCH 74/88] feat: handle input type date --- 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 f1259abc..6770c18b 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -61,7 +61,7 @@ export const getElementInformation = async ( ...info.attributes, selectedValue: selectElement.value, }; - } else if (element?.tagName === 'INPUT' && (element as HTMLInputElement).type === 'time') { + } else if (element?.tagName === 'INPUT' && (element as HTMLInputElement).type === 'time' || (element as HTMLInputElement).type === 'date') { info.innerText = (element as HTMLInputElement).value; } else { info.hasOnlyText = element?.children?.length === 0 && From 889107d892dad91d705746d22e425674989f5541 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Fri, 20 Dec 2024 17:23:59 +0530 Subject: [PATCH 75/88] chore: lint --- server/src/workflow-management/selector.ts | 158 ++++++++++----------- 1 file changed, 79 insertions(+), 79 deletions(-) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index 6770c18b..240f8921 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -53,16 +53,16 @@ export const getElementInformation = async ( info.url = (element as HTMLAnchorElement).href; info.innerText = element.innerText ?? ''; } else if (element?.tagName === 'IMG') { - info.imageUrl = (element as HTMLImageElement).src; - } else if (element?.tagName === 'SELECT') { - const selectElement = element as HTMLSelectElement; - info.innerText = selectElement.options[selectElement.selectedIndex]?.text ?? ''; - info.attributes = { + info.imageUrl = (element as HTMLImageElement).src; + } else if (element?.tagName === 'SELECT') { + const selectElement = element as HTMLSelectElement; + info.innerText = selectElement.options[selectElement.selectedIndex]?.text ?? ''; + info.attributes = { ...info.attributes, selectedValue: selectElement.value, - }; + }; } else if (element?.tagName === 'INPUT' && (element as HTMLInputElement).type === 'time' || (element as HTMLInputElement).type === 'date') { - info.innerText = (element as HTMLInputElement).value; + info.innerText = (element as HTMLInputElement).value; } else { info.hasOnlyText = element?.children?.length === 0 && element?.innerText?.length > 0; @@ -865,118 +865,118 @@ export const getNonUniqueSelectors = async (page: Page, coordinates: Coordinates try { if (!listSelector) { console.log(`NON UNIQUE: MODE 1`) - const selectors = await page.evaluate(({ x, y }: { x: number, y: number }) => { - function getNonUniqueSelector(element: HTMLElement): string { - let selector = element.tagName.toLowerCase(); + const selectors = await page.evaluate(({ x, y }: { x: number, y: number }) => { + function getNonUniqueSelector(element: HTMLElement): string { + let selector = element.tagName.toLowerCase(); - if (element.className) { - const classes = element.className.split(/\s+/).filter((cls: string) => Boolean(cls)); - if (classes.length > 0) { - const validClasses = classes.filter((cls: string) => !cls.startsWith('!') && !cls.includes(':')); - if (validClasses.length > 0) { - selector += '.' + validClasses.map(cls => CSS.escape(cls)).join('.'); + if (element.className) { + const classes = element.className.split(/\s+/).filter((cls: string) => Boolean(cls)); + if (classes.length > 0) { + const validClasses = classes.filter((cls: string) => !cls.startsWith('!') && !cls.includes(':')); + if (validClasses.length > 0) { + selector += '.' + validClasses.map(cls => CSS.escape(cls)).join('.'); + } } } + + return selector; } - return selector; - } + function getSelectorPath(element: HTMLElement | null): string { + const path: string[] = []; + let depth = 0; + const maxDepth = 2; - function getSelectorPath(element: HTMLElement | null): string { - const path: string[] = []; - let depth = 0; - const maxDepth = 2; + while (element && element !== document.body && depth < maxDepth) { + const selector = getNonUniqueSelector(element); + path.unshift(selector); + element = element.parentElement; + depth++; + } - while (element && element !== document.body && depth < maxDepth) { - const selector = getNonUniqueSelector(element); - path.unshift(selector); - element = element.parentElement; - depth++; + return path.join(' > '); } - return path.join(' > '); - } + const originalEl = document.elementFromPoint(x, y) as HTMLElement; + if (!originalEl) return null; - const originalEl = document.elementFromPoint(x, y) as HTMLElement; - if (!originalEl) return null; + let element = originalEl; - let element = originalEl; - - // if (listSelector === '') { + // if (listSelector === '') { while (element.parentElement) { const parentRect = element.parentElement.getBoundingClientRect(); const childRect = element.getBoundingClientRect(); - + const fullyContained = parentRect.left <= childRect.left && parentRect.right >= childRect.right && parentRect.top <= childRect.top && parentRect.bottom >= childRect.bottom; - + const significantOverlap = (childRect.width * childRect.height) / (parentRect.width * parentRect.height) > 0.5; - + if (fullyContained && significantOverlap) { element = element.parentElement; } else { break; } } - // } + // } - const generalSelector = getSelectorPath(element); - return { - generalSelector, - }; - }, coordinates); - return selectors || { generalSelector: '' }; - } else { - console.log(`NON UNIQUE: MODE 2`) - const selectors = await page.evaluate(({ x, y }: { x: number, y: number }) => { - function getNonUniqueSelector(element: HTMLElement): string { - let selector = element.tagName.toLowerCase(); + const generalSelector = getSelectorPath(element); + return { + generalSelector, + }; + }, coordinates); + return selectors || { generalSelector: '' }; + } else { + console.log(`NON UNIQUE: MODE 2`) + const selectors = await page.evaluate(({ x, y }: { x: number, y: number }) => { + function getNonUniqueSelector(element: HTMLElement): string { + let selector = element.tagName.toLowerCase(); - if (element.className) { - const classes = element.className.split(/\s+/).filter((cls: string) => Boolean(cls)); - if (classes.length > 0) { - const validClasses = classes.filter((cls: string) => !cls.startsWith('!') && !cls.includes(':')); - if (validClasses.length > 0) { - selector += '.' + validClasses.map(cls => CSS.escape(cls)).join('.'); + if (element.className) { + const classes = element.className.split(/\s+/).filter((cls: string) => Boolean(cls)); + if (classes.length > 0) { + const validClasses = classes.filter((cls: string) => !cls.startsWith('!') && !cls.includes(':')); + if (validClasses.length > 0) { + selector += '.' + validClasses.map(cls => CSS.escape(cls)).join('.'); + } } } + + return selector; } - return selector; - } + function getSelectorPath(element: HTMLElement | null): string { + const path: string[] = []; + let depth = 0; + const maxDepth = 2; - function getSelectorPath(element: HTMLElement | null): string { - const path: string[] = []; - let depth = 0; - const maxDepth = 2; + while (element && element !== document.body && depth < maxDepth) { + const selector = getNonUniqueSelector(element); + path.unshift(selector); + element = element.parentElement; + depth++; + } - while (element && element !== document.body && depth < maxDepth) { - const selector = getNonUniqueSelector(element); - path.unshift(selector); - element = element.parentElement; - depth++; + return path.join(' > '); } - return path.join(' > '); - } + const originalEl = document.elementFromPoint(x, y) as HTMLElement; + if (!originalEl) return null; - const originalEl = document.elementFromPoint(x, y) as HTMLElement; - if (!originalEl) return null; + let element = originalEl; - let element = originalEl; - - const generalSelector = getSelectorPath(element); - return { - generalSelector, - }; - }, coordinates); - return selectors || { generalSelector: '' }; - } + const generalSelector = getSelectorPath(element); + return { + generalSelector, + }; + }, coordinates); + return selectors || { generalSelector: '' }; + } } catch (error) { console.error('Error in getNonUniqueSelectors:', error); From a1f31ec05d00dbac4dcd12759d66005e3832031d Mon Sep 17 00:00:00 2001 From: amhsirak Date: Fri, 20 Dec 2024 17:44:10 +0530 Subject: [PATCH 76/88] feat: local setup upgrade cd step --- src/components/molecules/NavBar.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/molecules/NavBar.tsx b/src/components/molecules/NavBar.tsx index c2f271cf..54805ef7 100644 --- a/src/components/molecules/NavBar.tsx +++ b/src/components/molecules/NavBar.tsx @@ -25,7 +25,7 @@ export const NavBar: React.FC = ({ recordingName, isRecording }) => const navigate = useNavigate(); const [anchorEl, setAnchorEl] = useState(null); - const currentVersion = packageJson.version; + const currentVersion = "0.0.3" const [open, setOpen] = useState(false); const [latestVersion, setLatestVersion] = useState(null); @@ -208,6 +208,11 @@ export const NavBar: React.FC = ({ recordingName, isRecording }) =>

Run the commands below

+ # cd to project directory (eg: maxun) +
+ cd maxun +
+
# pull latest changes
git pull origin master From b6274cca14b878c9d0c00ef283c1d8ef00c381ec Mon Sep 17 00:00:00 2001 From: amhsirak Date: Fri, 20 Dec 2024 17:44:39 +0530 Subject: [PATCH 77/88] feat: docker setup upgrade cd step --- src/components/molecules/NavBar.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/components/molecules/NavBar.tsx b/src/components/molecules/NavBar.tsx index 54805ef7..0816b0e8 100644 --- a/src/components/molecules/NavBar.tsx +++ b/src/components/molecules/NavBar.tsx @@ -233,6 +233,11 @@ export const NavBar: React.FC = ({ recordingName, isRecording }) =>

Run the commands below

+ # cd to project directory (eg: maxun) +
+ cd maxun +
+
# pull latest docker images
docker-compose pull From 5b3b6d848f9ce33df328cd46bf4b2cb9a9684fcb Mon Sep 17 00:00:00 2001 From: amhsirak Date: Fri, 20 Dec 2024 17:45:49 +0530 Subject: [PATCH 78/88] feat: docker setup container down --- src/components/molecules/NavBar.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/components/molecules/NavBar.tsx b/src/components/molecules/NavBar.tsx index 0816b0e8..9acfe57b 100644 --- a/src/components/molecules/NavBar.tsx +++ b/src/components/molecules/NavBar.tsx @@ -238,6 +238,11 @@ export const NavBar: React.FC = ({ recordingName, isRecording }) => cd maxun

+ # stop the working containers +
+ docker-compose down +
+
# pull latest docker images
docker-compose pull From 6da2f6a130a562c5abf42785a9157161b8fc05d6 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Fri, 20 Dec 2024 17:47:28 +0530 Subject: [PATCH 79/88] feat: revert to version --- src/components/molecules/NavBar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/molecules/NavBar.tsx b/src/components/molecules/NavBar.tsx index 9acfe57b..24c41b20 100644 --- a/src/components/molecules/NavBar.tsx +++ b/src/components/molecules/NavBar.tsx @@ -25,7 +25,7 @@ export const NavBar: React.FC = ({ recordingName, isRecording }) => const navigate = useNavigate(); const [anchorEl, setAnchorEl] = useState(null); - const currentVersion = "0.0.3" + const currentVersion = packageJson.version; const [open, setOpen] = useState(false); const [latestVersion, setLatestVersion] = useState(null); From 2b96f07b36cd4f1a8ac35a54f49915f669b01220 Mon Sep 17 00:00:00 2001 From: RohitR311 Date: Fri, 20 Dec 2024 19:52:03 +0530 Subject: [PATCH 80/88] feat: add datetime-local selection pair to workflow --- .../workflow-management/classes/Generator.ts | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/server/src/workflow-management/classes/Generator.ts b/server/src/workflow-management/classes/Generator.ts index 9ff4922e..609541de 100644 --- a/server/src/workflow-management/classes/Generator.ts +++ b/server/src/workflow-management/classes/Generator.ts @@ -315,6 +315,26 @@ export class WorkflowGenerator { await this.addPairToWorkflowAndNotifyClient(pair, page); }; + public onDateTimeLocalSelection = async (page: Page, data: { selector: string, value: string }) => { + const { selector, value } = data; + + try { + await page.fill(selector, value); + } catch (error) { + console.error("Failed to fill datetime-local value:", error); + } + + const pair: WhereWhatPair = { + where: { url: this.getBestUrl(page.url()) }, + what: [{ + action: 'fill', + args: [selector, value], + }], + }; + + await this.addPairToWorkflowAndNotifyClient(pair, page); + }; + /** * Generates a pair for the click event. * @param coordinates The coordinates of the click event. @@ -390,6 +410,16 @@ export class WorkflowGenerator { return; } + const isDateTimeLocal = elementInfo?.tagName === 'INPUT' && elementInfo?.attributes?.type === 'datetime-local'; + + if (isDateTimeLocal) { + this.socket.emit('showDateTimePicker', { + coordinates, + selector + }); + return; + } + //const element = await getElementMouseIsOver(page, coordinates); //logger.log('debug', `Element: ${JSON.stringify(element, null, 2)}`); if (selector) { From 6d792f365ec5526431d13b08030c315ff5f6f623 Mon Sep 17 00:00:00 2001 From: RohitR311 Date: Fri, 20 Dec 2024 19:52:55 +0530 Subject: [PATCH 81/88] feat: add datetime-local selection event handlers --- server/src/browser-management/inputHandlers.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/server/src/browser-management/inputHandlers.ts b/server/src/browser-management/inputHandlers.ts index 982e18de..bf365053 100644 --- a/server/src/browser-management/inputHandlers.ts +++ b/server/src/browser-management/inputHandlers.ts @@ -260,6 +260,16 @@ const onTimeSelection = async (data: { selector: string, value: string }) => { await handleWrapper(handleTimeSelection, data); } +const handleDateTimeLocalSelection = async (generator: WorkflowGenerator, page: Page, data: { selector: string, value: string }) => { + await generator.onDateTimeLocalSelection(page, data); + logger.log('debug', `DateTime Local value ${data.value} selected`); +} + +const onDateTimeLocalSelection = async (data: { selector: string, value: string }) => { + logger.log('debug', 'Handling datetime-local selection event emitted from client'); + await handleWrapper(handleDateTimeLocalSelection, data); +} + /** * A wrapper function for handling the keyup event. * @param keyboardInput - the keyboard input of the keyup event @@ -418,6 +428,7 @@ const registerInputHandlers = (socket: Socket) => { socket.on("input:date", onDateSelection); socket.on("input:dropdown", onDropdownSelection); socket.on("input:time", onTimeSelection); + socket.on("input:datetime-local", onDateTimeLocalSelection); socket.on("action", onGenerateAction); }; From b1fbcb506c87783e8a953943df37dc2b56080ae5 Mon Sep 17 00:00:00 2001 From: RohitR311 Date: Fri, 20 Dec 2024 19:54:05 +0530 Subject: [PATCH 82/88] feat: trigger socket event to display datime-local picker --- src/components/atoms/canvas.tsx | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/components/atoms/canvas.tsx b/src/components/atoms/canvas.tsx index 77128a65..e71a4d93 100644 --- a/src/components/atoms/canvas.tsx +++ b/src/components/atoms/canvas.tsx @@ -6,6 +6,7 @@ import { useActionContext } from '../../context/browserActions'; import DatePicker from './DatePicker'; import Dropdown from './Dropdown'; import TimePicker from './TimePicker'; +import DateTimeLocalPicker from './DateTimeLocalPicker'; interface CreateRefCallback { (ref: React.RefObject): void; @@ -55,6 +56,11 @@ const Canvas = ({ width, height, onCreateRef }: CanvasProps) => { selector: string; } | null>(null); + const [dateTimeLocalInfo, setDateTimeLocalInfo] = React.useState<{ + coordinates: Coordinates; + selector: string; + } | null>(null); + const notifyLastAction = (action: string) => { if (lastAction !== action) { setLastAction(action); @@ -91,9 +97,15 @@ const Canvas = ({ width, height, onCreateRef }: CanvasProps) => { setTimePickerInfo(info); }); + socket.on('showDateTimePicker', (info: {coordinates: Coordinates, selector: string}) => { + setDateTimeLocalInfo(info); + }); + return () => { socket.off('showDatePicker'); socket.off('showDropdown'); + socket.off('showTimePicker'); + socket.off('showDateTimePicker'); }; } }, [socket]); @@ -222,6 +234,13 @@ const Canvas = ({ width, height, onCreateRef }: CanvasProps) => { onClose={() => setTimePickerInfo(null)} /> )} + {dateTimeLocalInfo && ( + setDateTimeLocalInfo(null)} + /> + )} ); From 15aa85976abd4f3f07f62c6f66b95087f047e31b Mon Sep 17 00:00:00 2001 From: RohitR311 Date: Fri, 20 Dec 2024 19:55:17 +0530 Subject: [PATCH 83/88] feat: add dateime-local picker component --- src/components/atoms/DateTimeLocalPicker.tsx | 74 ++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 src/components/atoms/DateTimeLocalPicker.tsx diff --git a/src/components/atoms/DateTimeLocalPicker.tsx b/src/components/atoms/DateTimeLocalPicker.tsx new file mode 100644 index 00000000..dc62a79b --- /dev/null +++ b/src/components/atoms/DateTimeLocalPicker.tsx @@ -0,0 +1,74 @@ +import React, { useState } from 'react'; +import { useSocketStore } from '../../context/socket'; +import { Coordinates } from './canvas'; + +interface DateTimeLocalPickerProps { + coordinates: Coordinates; + selector: string; + onClose: () => void; +} + +const DateTimeLocalPicker: React.FC = ({ coordinates, selector, onClose }) => { + const { socket } = useSocketStore(); + const [selectedDateTime, setSelectedDateTime] = useState(''); + + const handleDateTimeChange = (e: React.ChangeEvent) => { + setSelectedDateTime(e.target.value); + }; + + const handleConfirm = () => { + if (socket && selectedDateTime) { + socket.emit('input:datetime-local', { + selector, + value: selectedDateTime + }); + onClose(); + } + }; + + return ( +
+
+ +
+ + +
+
+
+ ); +}; + +export default DateTimeLocalPicker; \ No newline at end of file From 7cc9947b2acf2938ef7b799f7d535655b37158bc Mon Sep 17 00:00:00 2001 From: amhsirak Date: Fri, 20 Dec 2024 21:59:34 +0530 Subject: [PATCH 84/88] feat: auto logout after certain hours of inactivity --- src/context/auth.tsx | 88 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 80 insertions(+), 8 deletions(-) diff --git a/src/context/auth.tsx b/src/context/auth.tsx index 5c46e4de..a02711f3 100644 --- a/src/context/auth.tsx +++ b/src/context/auth.tsx @@ -1,4 +1,4 @@ -import { useReducer, createContext, useEffect } from 'react'; +import { useReducer, createContext, useEffect, useCallback } from 'react'; import axios from 'axios'; import { useNavigate } from 'react-router-dom'; import { apiUrl } from "../apiConfig"; @@ -14,12 +14,16 @@ interface ActionType { type InitialStateType = { user: any; + lastActivityTime?: number; }; const initialState = { user: null, + lastActivityTime: Date.now(), }; +const AUTO_LOGOUT_TIME = 4 * 60 * 60 * 1000; // 4 hours in milliseconds + const AuthContext = createContext<{ state: InitialStateType; dispatch: React.Dispatch; @@ -34,11 +38,13 @@ const reducer = (state: InitialStateType, action: ActionType) => { return { ...state, user: action.payload, + lastActivityTime: Date.now(), }; case 'LOGOUT': return { ...state, user: null, + lastActivityTime: undefined, }; default: return state; @@ -50,6 +56,39 @@ const AuthProvider = ({ children }: AuthProviderProps) => { const navigate = useNavigate(); axios.defaults.withCredentials = true; + const handleLogout = useCallback(async () => { + try { + await axios.get(`${apiUrl}/auth/logout`); + dispatch({ type: 'LOGOUT' }); + window.localStorage.removeItem('user'); + navigate('/login'); + } catch (err) { + console.error('Logout error:', err); + } + }, [navigate]); + + const checkAutoLogout = useCallback(() => { + if (state.user && state.lastActivityTime) { + const currentTime = Date.now(); + const timeSinceLastActivity = currentTime - state.lastActivityTime; + + if (timeSinceLastActivity >= AUTO_LOGOUT_TIME) { + handleLogout(); + } + } + }, [state.user, state.lastActivityTime, handleLogout]); + + // Update last activity time on user interactions + const updateActivityTime = useCallback(() => { + if (state.user) { + dispatch({ + type: 'LOGIN', + payload: state.user // Reuse existing user data + }); + } + }, [state.user]); + + // Initialize user from localStorage useEffect(() => { const storedUser = window.localStorage.getItem('user'); if (storedUser) { @@ -57,21 +96,54 @@ const AuthProvider = ({ children }: AuthProviderProps) => { } }, []); + // Set up activity listeners + useEffect(() => { + if (state.user) { + // List of events to track for user activity + const events = ['mousedown', 'keydown', 'scroll', 'touchstart']; + + // Throttled event handler + let timeoutId: NodeJS.Timeout; + const handleActivity = () => { + if (timeoutId) { + clearTimeout(timeoutId); + } + timeoutId = setTimeout(updateActivityTime, 1000); + }; + + // Add event listeners + events.forEach(event => { + window.addEventListener(event, handleActivity); + }); + + // Set up periodic check for auto logout + const checkInterval = setInterval(checkAutoLogout, 60000); // Check every minute + + // Cleanup + return () => { + events.forEach(event => { + window.removeEventListener(event, handleActivity); + }); + clearInterval(checkInterval); + if (timeoutId) { + clearTimeout(timeoutId); + } + }; + } + }, [state.user, updateActivityTime, checkAutoLogout]); + axios.interceptors.response.use( function (response) { return response; }, function (error) { const res = error.response; - if (res.status === 401 && res.config && !res.config.__isRetryRequest) { - return new Promise((resolve, reject) => { - axios - .get(`${apiUrl}/auth/logout`) + if (res?.status === 401 && res.config && !res.config.__isRetryRequest) { + return new Promise((_, reject) => { + handleLogout() .then(() => { console.log('/401 error > logout'); - dispatch({ type: 'LOGOUT' }); - window.localStorage.removeItem('user'); - navigate('/login'); + reject(error); }) .catch((err) => { console.error('AXIOS INTERCEPTORS ERROR:', err); From c0c10c8e80072426876ae695aa8dcbcc55d4ca9b Mon Sep 17 00:00:00 2001 From: Richard <115594235+richardzen@users.noreply.github.com> Date: Fri, 20 Dec 2024 22:18:09 +0530 Subject: [PATCH 85/88] feat: move COMMIT_CONVENTION.md --- .github/COMMIT_CONVENTION.md | 130 +++++++++++++++++++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 .github/COMMIT_CONVENTION.md diff --git a/.github/COMMIT_CONVENTION.md b/.github/COMMIT_CONVENTION.md new file mode 100644 index 00000000..076ff9cf --- /dev/null +++ b/.github/COMMIT_CONVENTION.md @@ -0,0 +1,130 @@ +## Git Commit Message Convention + +> This is adapted from [Conventional Commits 1.0.0](https://www.conventionalcommits.org/en/v1.0.0/). + +## Summary + +The Conventional Commits specification is a lightweight convention on top of commit messages. +It provides an easy set of rules for creating an explicit commit history; +which makes it easier to write automated tools on top of. +This convention dovetails with [SemVer](http://semver.org), +by describing the features, fixes, and breaking changes made in commit messages. + +The commit message should be structured as follows: + +--- + +``` +[optional scope]: +[optional body] +[optional footer(s)] +``` +--- + +
+The commit contains the following structural elements, to communicate intent to the +consumers of your library: + +1. **fix:** a commit of the _type_ `fix` patches a bug in your codebase (this correlates with [`PATCH`](http://semver.org/#summary) in Semantic Versioning). +1. **feat:** a commit of the _type_ `feat` introduces a new feature to the codebase (this correlates with [`MINOR`](http://semver.org/#summary) in Semantic Versioning). +1. **BREAKING CHANGE:** a commit that has a footer `BREAKING CHANGE:`, or appends a `!` after the type/scope, introduces a breaking API change (correlating with [`MAJOR`](http://semver.org/#summary) in Semantic Versioning). +A BREAKING CHANGE can be part of commits of any _type_. +1. _types_ other than `fix:` and `feat:` are allowed, for example [@commitlint/config-conventional](https://github.com/conventional-changelog/commitlint/tree/master/%40commitlint/config-conventional) (based on the [the Angular convention](https://github.com/angular/angular/blob/22b96b9/CONTRIBUTING.md#-commit-message-guidelines)) recommends `build:`, `chore:`, + `ci:`, `docs:`, `style:`, `refactor:`, `perf:`, `test:`, and others. +1. _footers_ other than `BREAKING CHANGE: ` may be provided and follow a convention similar to + [git trailer format](https://git-scm.com/docs/git-interpret-trailers). + +Additional types are not mandated by the Conventional Commits specification, and have no implicit effect in Semantic Versioning (unless they include a BREAKING CHANGE). +

+A scope may be provided to a commit's type, to provide additional contextual information and is contained within parenthesis, e.g., `feat(parser): add ability to parse arrays`. + +## Examples + +### Commit message with description and breaking change footer +``` +feat: allow provided config object to extend other configs + +BREAKING CHANGE: `extends` key in config file is now used for extending other config files +``` + +### Commit message with `!` to draw attention to breaking change +``` +feat!: send an email to the customer when a product is shipped +``` + +### Commit message with scope and `!` to draw attention to breaking change +``` +feat(api)!: send an email to the customer when a product is shipped +``` + +### Commit message with both `!` and BREAKING CHANGE footer +``` +chore!: drop support for Node 6 + +BREAKING CHANGE: use JavaScript features not available in Node 6. +``` + +### Commit message with no body +``` +docs: correct spelling of CHANGELOG +``` + +### Commit message with scope +``` +feat(lang): add polish language +``` + +### Commit message with multi-paragraph body and multiple footers +``` +fix: prevent racing of requests + +Introduce a request id and a reference to latest request. Dismiss +incoming responses other than from latest request. + +Remove timeouts which were used to mitigate the racing issue but are +obsolete now. + +Reviewed-by: Z +Refs: #123 +``` + +## Specification + +The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in [RFC 2119](https://www.ietf.org/rfc/rfc2119.txt). + +1. Commits MUST be prefixed with a type, which consists of a noun, `feat`, `fix`, etc., followed + by the OPTIONAL scope, OPTIONAL `!`, and REQUIRED terminal colon and space. +1. The type `feat` MUST be used when a commit adds a new feature to your application or library. +1. The type `fix` MUST be used when a commit represents a bug fix for your application. +1. A scope MAY be provided after a type. A scope MUST consist of a noun describing a + section of the codebase surrounded by parenthesis, e.g., `fix(parser):` +1. A description MUST immediately follow the colon and space after the type/scope prefix. +The description is a short summary of the code changes, e.g., _fix: array parsing issue when multiple spaces were contained in string_. +1. A longer commit body MAY be provided after the short description, providing additional contextual information about the code changes. The body MUST begin one blank line after the description. +1. A commit body is free-form and MAY consist of any number of newline separated paragraphs. +1. One or more footers MAY be provided one blank line after the body. Each footer MUST consist of + a word token, followed by either a `:` or `#` separator, followed by a string value (this is inspired by the + [git trailer convention](https://git-scm.com/docs/git-interpret-trailers)). +1. A footer's token MUST use `-` in place of whitespace characters, e.g., `Acked-by` (this helps differentiate + the footer section from a multi-paragraph body). An exception is made for `BREAKING CHANGE`, which MAY also be used as a token. +1. A footer's value MAY contain spaces and newlines, and parsing MUST terminate when the next valid footer + token/separator pair is observed. +1. Breaking changes MUST be indicated in the type/scope prefix of a commit, or as an entry in the + footer. +1. If included as a footer, a breaking change MUST consist of the uppercase text BREAKING CHANGE, followed by a colon, space, and description, e.g., +_BREAKING CHANGE: environment variables now take precedence over config files_. +1. If included in the type/scope prefix, breaking changes MUST be indicated by a + `!` immediately before the `:`. If `!` is used, `BREAKING CHANGE:` MAY be omitted from the footer section, + and the commit description SHALL be used to describe the breaking change. +1. Types other than `feat` and `fix` MAY be used in your commit messages, e.g., _docs: updated ref docs._ +1. The units of information that make up Conventional Commits MUST NOT be treated as case sensitive by implementors, with the exception of BREAKING CHANGE which MUST be uppercase. +1. BREAKING-CHANGE MUST be synonymous with BREAKING CHANGE, when used as a token in a footer. + +## Why Use Conventional Commits + +* Automatically generating CHANGELOGs. +* Automatically determining a semantic version bump (based on the types of commits landed). +* Communicating the nature of changes to teammates, the public, and other stakeholders. +* Triggering build and publish processes. +* Making it easier for people to contribute to your projects, by allowing them to explore + a more structured commit history. From 3f34a6c720ca05a34aab703eeb389e5fd86fcbd8 Mon Sep 17 00:00:00 2001 From: Richard <115594235+richardzen@users.noreply.github.com> Date: Fri, 20 Dec 2024 22:18:27 +0530 Subject: [PATCH 86/88] feat: delete .github/.github directory --- .github/.github/COMMIT_CONVENTION.md | 130 --------------------------- 1 file changed, 130 deletions(-) delete mode 100644 .github/.github/COMMIT_CONVENTION.md diff --git a/.github/.github/COMMIT_CONVENTION.md b/.github/.github/COMMIT_CONVENTION.md deleted file mode 100644 index 076ff9cf..00000000 --- a/.github/.github/COMMIT_CONVENTION.md +++ /dev/null @@ -1,130 +0,0 @@ -## Git Commit Message Convention - -> This is adapted from [Conventional Commits 1.0.0](https://www.conventionalcommits.org/en/v1.0.0/). - -## Summary - -The Conventional Commits specification is a lightweight convention on top of commit messages. -It provides an easy set of rules for creating an explicit commit history; -which makes it easier to write automated tools on top of. -This convention dovetails with [SemVer](http://semver.org), -by describing the features, fixes, and breaking changes made in commit messages. - -The commit message should be structured as follows: - ---- - -``` -[optional scope]: -[optional body] -[optional footer(s)] -``` ---- - -
-The commit contains the following structural elements, to communicate intent to the -consumers of your library: - -1. **fix:** a commit of the _type_ `fix` patches a bug in your codebase (this correlates with [`PATCH`](http://semver.org/#summary) in Semantic Versioning). -1. **feat:** a commit of the _type_ `feat` introduces a new feature to the codebase (this correlates with [`MINOR`](http://semver.org/#summary) in Semantic Versioning). -1. **BREAKING CHANGE:** a commit that has a footer `BREAKING CHANGE:`, or appends a `!` after the type/scope, introduces a breaking API change (correlating with [`MAJOR`](http://semver.org/#summary) in Semantic Versioning). -A BREAKING CHANGE can be part of commits of any _type_. -1. _types_ other than `fix:` and `feat:` are allowed, for example [@commitlint/config-conventional](https://github.com/conventional-changelog/commitlint/tree/master/%40commitlint/config-conventional) (based on the [the Angular convention](https://github.com/angular/angular/blob/22b96b9/CONTRIBUTING.md#-commit-message-guidelines)) recommends `build:`, `chore:`, - `ci:`, `docs:`, `style:`, `refactor:`, `perf:`, `test:`, and others. -1. _footers_ other than `BREAKING CHANGE: ` may be provided and follow a convention similar to - [git trailer format](https://git-scm.com/docs/git-interpret-trailers). - -Additional types are not mandated by the Conventional Commits specification, and have no implicit effect in Semantic Versioning (unless they include a BREAKING CHANGE). -

-A scope may be provided to a commit's type, to provide additional contextual information and is contained within parenthesis, e.g., `feat(parser): add ability to parse arrays`. - -## Examples - -### Commit message with description and breaking change footer -``` -feat: allow provided config object to extend other configs - -BREAKING CHANGE: `extends` key in config file is now used for extending other config files -``` - -### Commit message with `!` to draw attention to breaking change -``` -feat!: send an email to the customer when a product is shipped -``` - -### Commit message with scope and `!` to draw attention to breaking change -``` -feat(api)!: send an email to the customer when a product is shipped -``` - -### Commit message with both `!` and BREAKING CHANGE footer -``` -chore!: drop support for Node 6 - -BREAKING CHANGE: use JavaScript features not available in Node 6. -``` - -### Commit message with no body -``` -docs: correct spelling of CHANGELOG -``` - -### Commit message with scope -``` -feat(lang): add polish language -``` - -### Commit message with multi-paragraph body and multiple footers -``` -fix: prevent racing of requests - -Introduce a request id and a reference to latest request. Dismiss -incoming responses other than from latest request. - -Remove timeouts which were used to mitigate the racing issue but are -obsolete now. - -Reviewed-by: Z -Refs: #123 -``` - -## Specification - -The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in [RFC 2119](https://www.ietf.org/rfc/rfc2119.txt). - -1. Commits MUST be prefixed with a type, which consists of a noun, `feat`, `fix`, etc., followed - by the OPTIONAL scope, OPTIONAL `!`, and REQUIRED terminal colon and space. -1. The type `feat` MUST be used when a commit adds a new feature to your application or library. -1. The type `fix` MUST be used when a commit represents a bug fix for your application. -1. A scope MAY be provided after a type. A scope MUST consist of a noun describing a - section of the codebase surrounded by parenthesis, e.g., `fix(parser):` -1. A description MUST immediately follow the colon and space after the type/scope prefix. -The description is a short summary of the code changes, e.g., _fix: array parsing issue when multiple spaces were contained in string_. -1. A longer commit body MAY be provided after the short description, providing additional contextual information about the code changes. The body MUST begin one blank line after the description. -1. A commit body is free-form and MAY consist of any number of newline separated paragraphs. -1. One or more footers MAY be provided one blank line after the body. Each footer MUST consist of - a word token, followed by either a `:` or `#` separator, followed by a string value (this is inspired by the - [git trailer convention](https://git-scm.com/docs/git-interpret-trailers)). -1. A footer's token MUST use `-` in place of whitespace characters, e.g., `Acked-by` (this helps differentiate - the footer section from a multi-paragraph body). An exception is made for `BREAKING CHANGE`, which MAY also be used as a token. -1. A footer's value MAY contain spaces and newlines, and parsing MUST terminate when the next valid footer - token/separator pair is observed. -1. Breaking changes MUST be indicated in the type/scope prefix of a commit, or as an entry in the - footer. -1. If included as a footer, a breaking change MUST consist of the uppercase text BREAKING CHANGE, followed by a colon, space, and description, e.g., -_BREAKING CHANGE: environment variables now take precedence over config files_. -1. If included in the type/scope prefix, breaking changes MUST be indicated by a - `!` immediately before the `:`. If `!` is used, `BREAKING CHANGE:` MAY be omitted from the footer section, - and the commit description SHALL be used to describe the breaking change. -1. Types other than `feat` and `fix` MAY be used in your commit messages, e.g., _docs: updated ref docs._ -1. The units of information that make up Conventional Commits MUST NOT be treated as case sensitive by implementors, with the exception of BREAKING CHANGE which MUST be uppercase. -1. BREAKING-CHANGE MUST be synonymous with BREAKING CHANGE, when used as a token in a footer. - -## Why Use Conventional Commits - -* Automatically generating CHANGELOGs. -* Automatically determining a semantic version bump (based on the types of commits landed). -* Communicating the nature of changes to teammates, the public, and other stakeholders. -* Triggering build and publish processes. -* Making it easier for people to contribute to your projects, by allowing them to explore - a more structured commit history. From b39fca192571fc8db9cafb4d8a7f20d0b2ad69c7 Mon Sep 17 00:00:00 2001 From: Richard <115594235+richardzen@users.noreply.github.com> Date: Fri, 20 Dec 2024 22:22:33 +0530 Subject: [PATCH 87/88] chore: cleanup some code --- src/context/browserActions.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/context/browserActions.tsx b/src/context/browserActions.tsx index 55ca1b37..fd951f92 100644 --- a/src/context/browserActions.tsx +++ b/src/context/browserActions.tsx @@ -14,8 +14,8 @@ interface ActionContextProps { paginationType: PaginationType; limitType: LimitType; customLimit: string; - captureStage: CaptureStage; // New captureStage property - setCaptureStage: (stage: CaptureStage) => void; // Setter for captureStage + captureStage: CaptureStage; + setCaptureStage: (stage: CaptureStage) => void; startPaginationMode: () => void; startGetText: () => void; stopGetText: () => void; From 06f0fe6df1586aff5995f043a380b1d47077f9e8 Mon Sep 17 00:00:00 2001 From: Richard <115594235+richardzen@users.noreply.github.com> Date: Fri, 20 Dec 2024 22:27:04 +0530 Subject: [PATCH 88/88] chore: format `concurreny.ts` in maxun-core --- maxun-core/src/utils/concurrency.ts | 48 ++++++++++++++--------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/maxun-core/src/utils/concurrency.ts b/maxun-core/src/utils/concurrency.ts index e1ebb87b..56c15fd9 100644 --- a/maxun-core/src/utils/concurrency.ts +++ b/maxun-core/src/utils/concurrency.ts @@ -3,36 +3,36 @@ */ export default class Concurrency { /** - * Maximum number of workers running in parallel. If set to `null`, there is no limit. - */ + * Maximum number of workers running in parallel. If set to `null`, there is no limit. + */ maxConcurrency: number = 1; /** - * Number of currently active workers. - */ + * Number of currently active workers. + */ activeWorkers: number = 0; /** - * Queue of jobs waiting to be completed. - */ + * Queue of jobs waiting to be completed. + */ private jobQueue: Function[] = []; /** - * "Resolve" callbacks of the waitForCompletion() promises. - */ + * "Resolve" callbacks of the waitForCompletion() promises. + */ private waiting: Function[] = []; /** - * Constructs a new instance of concurrency manager. - * @param {number} maxConcurrency Maximum number of workers running in parallel. - */ + * Constructs a new instance of concurrency manager. + * @param {number} maxConcurrency Maximum number of workers running in parallel. + */ constructor(maxConcurrency: number) { this.maxConcurrency = maxConcurrency; } /** - * Takes a waiting job out of the queue and runs it. - */ + * Takes a waiting job out of the queue and runs it. + */ private runNextJob(): void { const job = this.jobQueue.pop(); @@ -53,12 +53,12 @@ export default class Concurrency { } /** - * Pass a job (a time-demanding async function) to the concurrency manager. \ - * The time of the job's execution depends on the concurrency manager itself - * (given a generous enough `maxConcurrency` value, it might be immediate, - * but this is not guaranteed). - * @param worker Async function to be executed (job to be processed). - */ + * Pass a job (a time-demanding async function) to the concurrency manager. \ + * The time of the job's execution depends on the concurrency manager itself + * (given a generous enough `maxConcurrency` value, it might be immediate, + * but this is not guaranteed). + * @param worker Async function to be executed (job to be processed). + */ addJob(job: () => Promise): void { // console.debug("Adding a worker!"); this.jobQueue.push(job); @@ -72,11 +72,11 @@ export default class Concurrency { } /** - * Waits until there is no running nor waiting job. \ - * If the concurrency manager is idle at the time of calling this function, - * it waits until at least one job is completed (can be "presubscribed"). - * @returns Promise, resolved after there is no running/waiting worker. - */ + * Waits until there is no running nor waiting job. \ + * If the concurrency manager is idle at the time of calling this function, + * it waits until at least one job is completed (can be "presubscribed"). + * @returns Promise, resolved after there is no running/waiting worker. + */ waitForCompletion(): Promise { return new Promise((res) => { this.waiting.push(res);