From 316531ea575d29348defc0d71c89754b029dab5f Mon Sep 17 00:00:00 2001 From: amhsirak Date: Wed, 20 Nov 2024 01:07:42 +0530 Subject: [PATCH 1/9] feat: traverse up the parent selector --- .../workflow-management/classes/Generator.ts | 2 ++ server/src/workflow-management/selector.ts | 20 ++++++++++++------- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/server/src/workflow-management/classes/Generator.ts b/server/src/workflow-management/classes/Generator.ts index 1716531c..1ecfac87 100644 --- a/server/src/workflow-management/classes/Generator.ts +++ b/server/src/workflow-management/classes/Generator.ts @@ -559,6 +559,8 @@ 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(`List sekector ${this.listSelector}`) } else { this.socket.emit('highlighter', { rect, selector: displaySelector, elementInfo }); } diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index 18b878ff..9bdcb072 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -790,20 +790,21 @@ export const getNonUniqueSelectors = async (page: Page, coordinates: Coordinates } }; - export const getChildSelectors = async (page: Page, parentSelector: string): Promise => { try { const childSelectors = await page.evaluate((parentSelector: string) => { + // Function to generate non-unique selector for an element function getNonUniqueSelector(element: HTMLElement): string { let selector = element.tagName.toLowerCase(); + // Add class names if available const className = typeof element.className === 'string' ? element.className : ''; if (className) { const classes = 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('.'); + selector += '.' + validClasses.map(cls => CSS.escape(cls)).join('.'); } } } @@ -811,30 +812,34 @@ export const getChildSelectors = async (page: Page, parentSelector: string): Pro return selector; } - function getSelectorPath(element: HTMLElement | null): string { + // Function to get the full CSS selector path from the current element to the parentSelector + function getSelectorPath(element: HTMLElement | null, parentElement: HTMLElement): string { if (!element || !element.parentElement) return ''; - const parentSelector = getNonUniqueSelector(element.parentElement); + const parentSelector = getNonUniqueSelector(parentElement); const elementSelector = getNonUniqueSelector(element); return `${parentSelector} > ${elementSelector}`; } - function getAllDescendantSelectors(element: HTMLElement, stopAtParent: HTMLElement | null): string[] { + // Function to recursively gather all descendant selectors + function getAllDescendantSelectors(element: HTMLElement, parentElement: HTMLElement): string[] { let selectors: string[] = []; const children = Array.from(element.children) as HTMLElement[]; for (const child of children) { - selectors.push(getSelectorPath(child)); - selectors = selectors.concat(getAllDescendantSelectors(child, stopAtParent)); + selectors.push(getSelectorPath(child, parentElement)); + selectors = selectors.concat(getAllDescendantSelectors(child, parentElement)); } return selectors; } + // Get the parent element based on the selector const parentElement = document.querySelector(parentSelector) as HTMLElement; if (!parentElement) return []; + // Gather all descendant selectors starting from the parent element return getAllDescendantSelectors(parentElement, parentElement); }, parentSelector); @@ -845,6 +850,7 @@ export const getChildSelectors = async (page: Page, parentSelector: string): Pro } }; + /** * Returns the first pair from the given workflow that contains the given selector * inside the where condition, and it is the only selector there. From a1d89d94ff048ebc5d21172bd2992415ed133a35 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Wed, 20 Nov 2024 02:37:19 +0530 Subject: [PATCH 2/9] feat: use outerHTMl --- .../workflow-management/classes/Generator.ts | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/server/src/workflow-management/classes/Generator.ts b/server/src/workflow-management/classes/Generator.ts index 1ecfac87..72580fa7 100644 --- a/server/src/workflow-management/classes/Generator.ts +++ b/server/src/workflow-management/classes/Generator.ts @@ -543,6 +543,22 @@ export class WorkflowGenerator { return bestSelector; } + public getOuterHTML = async (page: Page, parentSelector: string): Promise => { + try { + const outerHTML = await page.evaluate((parentSelector: string) => { + const parentElement = document.querySelector(parentSelector); + return parentElement ? parentElement.outerHTML : ''; + }, parentSelector); + + console.log(`Outer html: ${outerHTML}`) + return outerHTML; + } catch (error) { + console.error('Error in getOuterHTML:', error); + return ''; + } + }; + + /** * Generates data for highlighting the element on client side and emits the * highlighter event to the client. @@ -557,10 +573,10 @@ export class WorkflowGenerator { if (rect) { if (this.getList === true) { if (this.listSelector !== '') { - const childSelectors = await getChildSelectors(page, this.listSelector || ''); + const childSelectors = await getChildSelectors(page, await this.getOuterHTML(page, this.listSelector)); this.socket.emit('highlighter', { rect, selector: displaySelector, elementInfo, childSelectors }) console.log(`Child Selectors: ${childSelectors}`) - console.log(`List sekector ${this.listSelector}`) + console.log(`Parent Selector: ${this.listSelector}`) } else { this.socket.emit('highlighter', { rect, selector: displaySelector, elementInfo }); } From c704a644a0e67eaa0a4fff87e9087db5f2174465 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Wed, 20 Nov 2024 02:37:54 +0530 Subject: [PATCH 3/9] feat: use outerHTML & generate more meaningful semantic selectors --- server/src/workflow-management/selector.ts | 103 +++++++++++++-------- 1 file changed, 65 insertions(+), 38 deletions(-) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index 9bdcb072..78965530 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -790,67 +790,94 @@ export const getNonUniqueSelectors = async (page: Page, coordinates: Coordinates } }; -export const getChildSelectors = async (page: Page, parentSelector: string): Promise => { + +export const getChildSelectors = async (page: Page, outerHTML: string): Promise => { try { - const childSelectors = await page.evaluate((parentSelector: string) => { - // Function to generate non-unique selector for an element + const childSelectors = await page.evaluate((outerHTML: string) => { function getNonUniqueSelector(element: HTMLElement): string { + // Start with tag name let selector = element.tagName.toLowerCase(); - // Add class names if available - const className = typeof element.className === 'string' ? element.className : ''; - if (className) { - const classes = 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('.'); + // Add meaningful attributes + const attributesToConsider = [ + 'class', + 'data-testid', + 'data-cy', + 'data-test', + 'aria-label', + 'title', + 'id' + ]; + + // Collect additional attributes + const additionalAttrs: string[] = []; + + attributesToConsider.forEach(attrName => { + if (attrName === 'class') { + // Handle classes + const className = typeof element.className === 'string' ? element.className : ''; + if (className) { + const classes = className.split(/\s+/) + .filter((cls: string) => Boolean(cls)) + .filter(cls => !cls.startsWith('!') && !cls.includes(':')); + + if (classes.length > 0) { + additionalAttrs.push( + classes.map(cls => `.${CSS.escape(cls)}`).join('') + ); + } + } + } else { + // Handle other attributes + const attrValue = element.getAttribute(attrName); + if (attrValue) { + additionalAttrs.push(`[${attrName}="${CSS.escape(attrValue)}"]`); } } - } + }); - return selector; + // Combine selector with attributes + return selector + additionalAttrs.join(''); } - // Function to get the full CSS selector path from the current element to the parentSelector - function getSelectorPath(element: HTMLElement | null, parentElement: HTMLElement): string { - if (!element || !element.parentElement) return ''; - - const parentSelector = getNonUniqueSelector(parentElement); + function getSelectorPath(element: HTMLElement | null, root: HTMLElement): string { + if (!element || element === root) return ''; + + const parentSelector = getSelectorPath(element.parentElement, root); const elementSelector = getNonUniqueSelector(element); - return `${parentSelector} > ${elementSelector}`; + return parentSelector ? `${parentSelector} ${elementSelector}` : elementSelector; } - // Function to recursively gather all descendant selectors - function getAllDescendantSelectors(element: HTMLElement, parentElement: HTMLElement): string[] { - let selectors: string[] = []; - const children = Array.from(element.children) as HTMLElement[]; - - for (const child of children) { - selectors.push(getSelectorPath(child, parentElement)); - selectors = selectors.concat(getAllDescendantSelectors(child, parentElement)); - } - - return selectors; + function parseOuterHTML(outerHTML: string): HTMLElement { + const tempContainer = document.createElement('div'); + tempContainer.innerHTML = outerHTML.trim(); + return tempContainer.firstElementChild as HTMLElement; } - // Get the parent element based on the selector - const parentElement = document.querySelector(parentSelector) as HTMLElement; - if (!parentElement) return []; + function getAllDescendantSelectors(root: HTMLElement): string[] { + const descendants = root.querySelectorAll('*'); + const selectors = Array.from(descendants).map(element => + getSelectorPath(element as HTMLElement, root) + ); + + // Include the root element itself + const rootSelector = getNonUniqueSelector(root); + return [rootSelector, ...selectors]; + } - // Gather all descendant selectors starting from the parent element - return getAllDescendantSelectors(parentElement, parentElement); - }, parentSelector); + const rootElement = parseOuterHTML(outerHTML); + if (!rootElement) return []; + return getAllDescendantSelectors(rootElement); + }, outerHTML); return childSelectors || []; } catch (error) { - console.error('Error in getChildSelectors:', error); + console.error('Error in getChildSelectorsFromOuterHTML:', error); return []; } }; - /** * Returns the first pair from the given workflow that contains the given selector * inside the where condition, and it is the only selector there. From 18ca1bef18082e097c1bc0fd64326bccdb0899a1 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Wed, 20 Nov 2024 03:46:09 +0530 Subject: [PATCH 4/9] feat: revert to listSelectpr --- .../workflow-management/classes/Generator.ts | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/server/src/workflow-management/classes/Generator.ts b/server/src/workflow-management/classes/Generator.ts index 72580fa7..7801a20e 100644 --- a/server/src/workflow-management/classes/Generator.ts +++ b/server/src/workflow-management/classes/Generator.ts @@ -543,22 +543,6 @@ export class WorkflowGenerator { return bestSelector; } - public getOuterHTML = async (page: Page, parentSelector: string): Promise => { - try { - const outerHTML = await page.evaluate((parentSelector: string) => { - const parentElement = document.querySelector(parentSelector); - return parentElement ? parentElement.outerHTML : ''; - }, parentSelector); - - console.log(`Outer html: ${outerHTML}`) - return outerHTML; - } catch (error) { - console.error('Error in getOuterHTML:', error); - return ''; - } - }; - - /** * Generates data for highlighting the element on client side and emits the * highlighter event to the client. @@ -573,7 +557,7 @@ export class WorkflowGenerator { if (rect) { if (this.getList === true) { if (this.listSelector !== '') { - const childSelectors = await getChildSelectors(page, await this.getOuterHTML(page, 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}`) From 0765a1a36345e67b5210fca7c57dc71258e9ddb1 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Wed, 20 Nov 2024 03:46:33 +0530 Subject: [PATCH 5/9] feat: revert to listSelector --- server/src/workflow-management/selector.ts | 109 +++++++++------------ 1 file changed, 45 insertions(+), 64 deletions(-) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index 78965530..193de891 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -791,93 +791,74 @@ export const getNonUniqueSelectors = async (page: Page, coordinates: Coordinates }; -export const getChildSelectors = async (page: Page, outerHTML: string): Promise => { +export const getChildSelectors = async (page: Page, parentSelector: string): Promise => { try { - const childSelectors = await page.evaluate((outerHTML: string) => { + const childSelectors = await page.evaluate((parentSelector: string) => { + // Function to get a non-unique selector based on tag and class (if present) function getNonUniqueSelector(element: HTMLElement): string { - // Start with tag name let selector = element.tagName.toLowerCase(); - // Add meaningful attributes - const attributesToConsider = [ - 'class', - 'data-testid', - 'data-cy', - 'data-test', - 'aria-label', - 'title', - 'id' - ]; - - // Collect additional attributes - const additionalAttrs: string[] = []; - - attributesToConsider.forEach(attrName => { - if (attrName === 'class') { - // Handle classes - const className = typeof element.className === 'string' ? element.className : ''; - if (className) { - const classes = className.split(/\s+/) - .filter((cls: string) => Boolean(cls)) - .filter(cls => !cls.startsWith('!') && !cls.includes(':')); - - if (classes.length > 0) { - additionalAttrs.push( - classes.map(cls => `.${CSS.escape(cls)}`).join('') - ); - } - } - } else { - // Handle other attributes - const attrValue = element.getAttribute(attrName); - if (attrValue) { - additionalAttrs.push(`[${attrName}="${CSS.escape(attrValue)}"]`); + const className = typeof element.className === 'string' ? element.className : ''; + if (className) { + const classes = 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('.'); } } - }); + } - // Combine selector with attributes - return selector + additionalAttrs.join(''); + return selector; } - function getSelectorPath(element: HTMLElement | null, root: HTMLElement): string { - if (!element || element === root) return ''; - - const parentSelector = getSelectorPath(element.parentElement, root); + // Function to generate selector path from an element to its parent + function getSelectorPath(element: HTMLElement | null): string { + if (!element || !element.parentElement) return ''; + + const parentSelector = getNonUniqueSelector(element.parentElement); const elementSelector = getNonUniqueSelector(element); - return parentSelector ? `${parentSelector} ${elementSelector}` : elementSelector; + return `${parentSelector} > ${elementSelector}`; } - function parseOuterHTML(outerHTML: string): HTMLElement { - const tempContainer = document.createElement('div'); - tempContainer.innerHTML = outerHTML.trim(); - return tempContainer.firstElementChild as HTMLElement; + // Function to recursively get all descendant selectors + function getAllDescendantSelectors(element: HTMLElement): string[] { + let selectors: string[] = []; + const children = Array.from(element.children) as HTMLElement[]; + + for (const child of children) { + const childPath = getSelectorPath(child); + if (childPath) { + selectors.push(childPath); // Add direct child path + selectors = selectors.concat(getAllDescendantSelectors(child)); // Recursively process descendants + } + } + + return selectors; } - function getAllDescendantSelectors(root: HTMLElement): string[] { - const descendants = root.querySelectorAll('*'); - const selectors = Array.from(descendants).map(element => - getSelectorPath(element as HTMLElement, root) - ); - - // Include the root element itself - const rootSelector = getNonUniqueSelector(root); - return [rootSelector, ...selectors]; - } + // Find all occurrences of the parent selector in the DOM + const parentElements = Array.from(document.querySelectorAll(parentSelector)) as HTMLElement[]; + const allChildSelectors = new Set(); // Use a set to ensure uniqueness - const rootElement = parseOuterHTML(outerHTML); - if (!rootElement) return []; - return getAllDescendantSelectors(rootElement); - }, outerHTML); + // Process each parent element and its descendants + parentElements.forEach((parentElement) => { + const descendantSelectors = getAllDescendantSelectors(parentElement); + descendantSelectors.forEach((selector) => allChildSelectors.add(selector)); // Add selectors to the set + }); + + return Array.from(allChildSelectors); // Convert the set back to an array + }, parentSelector); return childSelectors || []; } catch (error) { - console.error('Error in getChildSelectorsFromOuterHTML:', error); + console.error('Error in getChildSelectors:', error); return []; } }; + /** * Returns the first pair from the given workflow that contains the given selector * inside the where condition, and it is the only selector there. From 9172e0343c55bd1886d9cebb48701ad79ad08b6f Mon Sep 17 00:00:00 2001 From: amhsirak Date: Wed, 20 Nov 2024 05:14:03 +0530 Subject: [PATCH 6/9] feat: handle empty rows of data --- src/components/molecules/RunContent.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/molecules/RunContent.tsx b/src/components/molecules/RunContent.tsx index ff22a3a8..445cfac1 100644 --- a/src/components/molecules/RunContent.tsx +++ b/src/components/molecules/RunContent.tsx @@ -140,7 +140,9 @@ export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRe {tableData.map((row, index) => ( {columns.map((column) => ( - {row[column]} + + {row[column] === undefined || row[column] === "" ? "-" : row[column]} + ))} ))} From 0606c3b4a8e44fd85819bcce6945ffce8119ddc1 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Wed, 20 Nov 2024 05:14:18 +0530 Subject: [PATCH 7/9] feat: del empty rows --- src/components/molecules/RunContent.tsx | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/src/components/molecules/RunContent.tsx b/src/components/molecules/RunContent.tsx index 445cfac1..0c163368 100644 --- a/src/components/molecules/RunContent.tsx +++ b/src/components/molecules/RunContent.tsx @@ -31,18 +31,23 @@ export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRe setTab(tab); }, [interpretationInProgress]); - useEffect(() => { - if (row.serializableOutput && Object.keys(row.serializableOutput).length > 0) { - const firstKey = Object.keys(row.serializableOutput)[0]; - const data = row.serializableOutput[firstKey]; - if (Array.isArray(data)) { - setTableData(data); - if (data.length > 0) { - setColumns(Object.keys(data[0])); - } +useEffect(() => { + if (row.serializableOutput && Object.keys(row.serializableOutput).length > 0) { + const firstKey = Object.keys(row.serializableOutput)[0]; + const data = row.serializableOutput[firstKey]; + if (Array.isArray(data)) { + // Filter out completely empty rows + const filteredData = data.filter(row => + Object.values(row).some(value => value !== undefined && value !== "") + ); + setTableData(filteredData); + if (filteredData.length > 0) { + setColumns(Object.keys(filteredData[0])); } } - }, [row.serializableOutput]); + } +}, [row.serializableOutput]); + // Function to convert table data to CSV format const convertToCSV = (data: any[], columns: string[]): string => { From f5154b21cca0f9aa765d42b6bc911c5daee54ded Mon Sep 17 00:00:00 2001 From: amhsirak Date: Wed, 20 Nov 2024 05:14:34 +0530 Subject: [PATCH 8/9] chore: lint --- src/components/molecules/RunContent.tsx | 28 ++++++++++++------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/components/molecules/RunContent.tsx b/src/components/molecules/RunContent.tsx index 0c163368..c0beb51e 100644 --- a/src/components/molecules/RunContent.tsx +++ b/src/components/molecules/RunContent.tsx @@ -31,22 +31,22 @@ export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRe setTab(tab); }, [interpretationInProgress]); -useEffect(() => { - if (row.serializableOutput && Object.keys(row.serializableOutput).length > 0) { - const firstKey = Object.keys(row.serializableOutput)[0]; - const data = row.serializableOutput[firstKey]; - if (Array.isArray(data)) { - // Filter out completely empty rows - const filteredData = data.filter(row => - Object.values(row).some(value => value !== undefined && value !== "") - ); - setTableData(filteredData); - if (filteredData.length > 0) { - setColumns(Object.keys(filteredData[0])); + useEffect(() => { + if (row.serializableOutput && Object.keys(row.serializableOutput).length > 0) { + const firstKey = Object.keys(row.serializableOutput)[0]; + const data = row.serializableOutput[firstKey]; + if (Array.isArray(data)) { + // Filter out completely empty rows + const filteredData = data.filter(row => + Object.values(row).some(value => value !== undefined && value !== "") + ); + setTableData(filteredData); + if (filteredData.length > 0) { + setColumns(Object.keys(filteredData[0])); + } } } - } -}, [row.serializableOutput]); + }, [row.serializableOutput]); // Function to convert table data to CSV format From 4844e63358c65877f4e5f56a6b665df0aec1dd7e Mon Sep 17 00:00:00 2001 From: amhsirak Date: Wed, 20 Nov 2024 05:14:57 +0530 Subject: [PATCH 9/9] chore: -rm comment --- src/components/molecules/RunContent.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/molecules/RunContent.tsx b/src/components/molecules/RunContent.tsx index c0beb51e..ff414628 100644 --- a/src/components/molecules/RunContent.tsx +++ b/src/components/molecules/RunContent.tsx @@ -58,7 +58,6 @@ export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRe return [header, ...rows].join('\n'); }; - // Function to download CSV file when called const downloadCSV = () => { const csvContent = convertToCSV(tableData, columns); const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });