Skip to content

Commit

Permalink
Merge pull request #1672 from jdi-testing/issue_1657-enrich-document-…
Browse files Browse the repository at this point in the history
…with-inline-styles-and-JS

Issue 1657: enrich document with inline styles and js
  • Loading branch information
Iogsotot authored Mar 11, 2024
2 parents 2ae002a + 18aacfe commit 275effa
Show file tree
Hide file tree
Showing 7 changed files with 171 additions and 94 deletions.
2 changes: 1 addition & 1 deletion manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"name": "JDN",
"description": "JDN – helps Test Automation Engineer to create Page Objects in the test automation framework and speed up test development",
"devtools_page": "index.html",
"version": "3.15.8",
"version": "3.15.9",
"icons": {
"128": "icon128.png"
},
Expand Down
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "jdn-ai-chrome-extension",
"version": "3.15.8",
"version": "3.15.9",
"description": "jdn-ai chrome extension",
"scripts": {
"start": "webpack --watch --env devenv",
Expand Down
62 changes: 62 additions & 0 deletions src/common/utils/getFullDocumentWithStyles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import connector from '../../pageServices/connector';

async function waitForAllStylesToLoad() {
const styleSheets = document.querySelectorAll('link[rel="stylesheet"]');

const loadPromises = Array.from(styleSheets).map(
(sheet) =>
new Promise<void>((resolve) => {
const linkElement = sheet as HTMLLinkElement;
if (linkElement.sheet) {
resolve();
} else {
linkElement.onload = () => resolve();
linkElement.onerror = () => resolve();
}
}),
);

await Promise.all(loadPromises);
}

export const getFullDocumentWithStyles = async () => {
await waitForAllStylesToLoad();

const documentResult = await connector.attachContentScript(() => {
const fetchCSS = () => {
let allStyles = '';
for (let i = 0; i < document.styleSheets.length; i++) {
const sheet = document.styleSheets[i];
try {
const rules = sheet.rules || sheet.cssRules;
for (let j = 0; j < rules.length; j++) {
allStyles += rules[j].cssText + '\n';
}
} catch (e) {
console.error("Can't fetch styles: ", e);
}
}
return allStyles;
};

let outerHTML = document.documentElement.outerHTML;

const stylesString = fetchCSS();
outerHTML = outerHTML.replace(/<link rel="stylesheet"[^>]+>/g, '');
outerHTML = outerHTML.replace('</head>', `<style>\n${stylesString}</style></head>`);

const scripts = [...document.scripts]
.map((script: HTMLScriptElement) => {
return script.src ? `<script src="${script.src}"></script>` : `<script>${script.innerHTML}</script>`;
})
.join('\n');

outerHTML = outerHTML.replace('</body>', `${scripts}</body>`);

return JSON.stringify({ outerHTML });
});

const result = await documentResult[0].result;
const { outerHTML } = JSON.parse(result);
return JSON.stringify(outerHTML);
};
68 changes: 42 additions & 26 deletions src/pageServices/connector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export interface ScriptMessage {
param: any;
}

/* global chrome */
///* global chrome */

/* eslint-disable */
// because we don't have proper typings for chrome object
Expand All @@ -24,7 +24,7 @@ class Connector {
onmessage: (
payload: { message: ScriptMsg; param: Record<string, never> },
sender: chrome.runtime.MessageSender,
sendResponse: (response: any) => void
sendResponse: (response: any) => void,
) => void;
onDisconnectCallback: () => void;

Expand Down Expand Up @@ -52,8 +52,15 @@ class Connector {
.then((response) => response)
.catch((error: Error) => {
if (error.message === SCRIPT_ERROR.NO_RESPONSE && isUndefined(onResponse)) return null;
if (error.message === SCRIPT_ERROR.NO_CONNECTION && action !== ScriptMsg.PingScript && action !== ScriptMsg.CheckSession && action !== ScriptMsg.KillHighlight) {
return this.onDisconnectHandler(this.port, true).then(() => this.sendMessage(action, payload, onResponse, tabId));
if (
error.message === SCRIPT_ERROR.NO_CONNECTION &&
action !== ScriptMsg.PingScript &&
action !== ScriptMsg.CheckSession &&
action !== ScriptMsg.KillHighlight
) {
return this.onDisconnectHandler(this.port, true).then(() =>
this.sendMessage(action, payload, onResponse, tabId),
);
}
return error;
})
Expand All @@ -71,8 +78,8 @@ class Connector {
callback: (
payload: ScriptMessagePayload,
sender: chrome.runtime.MessageSender,
sendResponse: (response: any) => void
) => void
sendResponse: (response: any) => void,
) => void,
) {
if (this.onmessage) chrome.runtime.onMessage.removeListener(this.onmessage);
this.onmessage = callback;
Expand All @@ -81,7 +88,7 @@ class Connector {

async onDisconnectHandler(port: any, forced?: boolean) {
if (this.port) this.port = undefined;
if (typeof this.onDisconnectCallback === "function" && !forced) this.onDisconnectCallback();
if (typeof this.onDisconnectCallback === 'function' && !forced) this.onDisconnectCallback();
return this.initScripts().then(() => this.port?.onDisconnect.addListener(this.onDisconnectHandler.bind(this)));
}

Expand All @@ -101,11 +108,11 @@ class Connector {
}
}

async attachContentScript(script: string[] | ((...args: any[]) => void), scriptName = "") {
async attachContentScript(script: string[] | ((...args: any[]) => void), scriptName = '') {
return this.scriptExists(scriptName).then((result) => {
if (result) return true;

const injection = typeof script === "function" ? { func: script } : { files: script };
const injection = typeof script === 'function' ? { func: script } : { files: script };

return chrome.scripting
.executeScript({
Expand Down Expand Up @@ -133,14 +140,14 @@ class Connector {

attachStaticScripts() {
return Promise.all([
this.attachContentScript(["contentScript.bundle.js"], "index").then(() => {
this.attachContentScript(['contentScript.bundle.js'], 'index').then(() => {
this.createPort();
chrome.storage.sync.set({ IS_DISCONNECTED: false });
sendMessage.defineTabId(this.tabId);
sendMessage.setClosedSession({ tabId: this.tabId, isClosed: false });
return "success";
return 'success';
}),
this.attachCSS("contentStyles.css"),
this.attachCSS('contentStyles.css'),
]);
}

Expand All @@ -160,41 +167,50 @@ const connector = new Connector();
export const sendMessage = {
addElement: (el: ILocator) => connector.sendMessage(ScriptMsg.AddElement, el),
assignDataLabels: (payload: PredictedEntity[]) => connector.sendMessage(ScriptMsg.AssignDataLabel, payload),
assignJdnHash: (payload: { jdnHash: string, locatorValue: string, isCSSLocator?: Boolean }) => connector.sendMessage(ScriptMsg.AssignJdnHash, payload),
assignJdnHash: (payload: { jdnHash: string; locatorValue: string; isCSSLocator?: Boolean }) =>
connector.sendMessage(ScriptMsg.AssignJdnHash, payload),
assignParents: (payload: ILocator[]) => connector.sendMessage(ScriptMsg.AssignParents, payload),
changeElementName: (el: ILocator) => connector.sendMessage(ScriptMsg.ChangeElementName, el),
changeElementType: (el: ILocator) => connector.sendMessage(ScriptMsg.ChangeElementType, el),
changeStatus: (el: ILocator) => connector.sendMessage(ScriptMsg.ChangeStatus, el),
checkSession: (payload: null, onResponse?: () => void): Promise<{ message: string; tabId: number }[]> =>
connector.sendMessageToAllTabs(ScriptMsg.CheckSession, payload, onResponse),
defineTabId: (payload: number) => connector.sendMessage(ScriptMsg.DefineTabId, payload),
evaluateXpath: (payload: { xPath: string; element_id?: ElementId, originJdnHash?: string }, onResponse?: () => void) =>
connector.sendMessage(ScriptMsg.EvaluateXpath, payload, onResponse),
evaluateStandardLocator: (payload: { selector: string; locatorType: LocatorType, element_id?: ElementId, originJdnHash?: string }, onResponse?: () => void) =>
connector.sendMessage(ScriptMsg.EvaluateStandardLocator, payload, onResponse),
getPageData: (payload?: {}, onResponse?: () => void) => connector.sendMessage(ScriptMsg.GetPageData, payload, onResponse),
generateSelectorByHash: (payload: { element_id: string, jdnHash: string }, onResponse?: () => void) =>
evaluateXpath: (
payload: { xPath: string; element_id?: ElementId; originJdnHash?: string },
onResponse?: () => void,
) => connector.sendMessage(ScriptMsg.EvaluateXpath, payload, onResponse),
evaluateStandardLocator: (
payload: { selector: string; locatorType: LocatorType; element_id?: ElementId; originJdnHash?: string },
onResponse?: () => void,
) => connector.sendMessage(ScriptMsg.EvaluateStandardLocator, payload, onResponse),
getPageData: (payload?: {}, onResponse?: () => void) =>
connector.sendMessage(ScriptMsg.GetPageData, payload, onResponse),
generateSelectorByHash: (payload: { element_id: string; jdnHash: string }, onResponse?: () => void) =>
connector.sendMessage(ScriptMsg.GenerateSelectorByHash, payload, onResponse),
generateSelectorGroupByHash: (payload: { elements: ILocator[], fireCallbackMessage?: boolean }, onResponse?: () => void) =>
connector.sendMessage(ScriptMsg.GenerateSelectorGroupByHash, payload, onResponse),
generateSelectorGroupByHash: (
payload: { elements: ILocator[]; fireCallbackMessage?: boolean },
onResponse?: () => void,
) => connector.sendMessage(ScriptMsg.GenerateSelectorGroupByHash, payload, onResponse),
findBySelectors: (payload: SelectorsMap) => connector.sendMessage(ScriptMsg.FindBySelectors, payload),
setClosedSession: (payload: { tabId: number; isClosed: boolean }) =>
connector.sendMessage(ScriptMsg.SetClosedSession, payload),
setHighlight: (payload: { elements?: ILocator[]; filter?: ClassFilterValue; isAlreadyGenerated?: boolean }) =>
connector.sendMessage(ScriptMsg.SetHighlight, payload),
killHighlight: (payload?: {}, onResponse?: () => void) => connector.sendMessage(ScriptMsg.KillHighlight, null, onResponse),
generateAttributes: (payload: { elements: PredictedEntity[], generateCss: boolean }, onResponse?: () => void) =>
killHighlight: (payload?: {}, onResponse?: () => void) =>
connector.sendMessage(ScriptMsg.KillHighlight, null, onResponse),
generateAttributes: (payload: { elements: PredictedEntity[]; generateCss: boolean }, onResponse?: () => void) =>
connector.sendMessage(ScriptMsg.GenerateAttributes, payload, onResponse),
getElementXpath: (payload: string, onResponse?: () => void) =>
connector.sendMessage(ScriptMsg.GetElementXpath, payload, onResponse),
pingScript: (payload: { scriptName: string }, onResponse?: () => void) =>
connector.sendMessage(ScriptMsg.PingScript, payload, onResponse),
removeElement: (payload: ILocator) => connector.sendMessage(ScriptMsg.RemoveElement, payload),
setActive: (payload: ILocator | ILocator[]) => connector.sendMessage(ScriptMsg.SetActive, payload),
toggle: (payload: { element: ILocator; skipScroll?: boolean }) => connector.sendMessage(ScriptMsg.HighlightToggled, payload),
toggle: (payload: { element: ILocator; skipScroll?: boolean }) =>
connector.sendMessage(ScriptMsg.HighlightToggled, payload),
toggleDeleted: (el: ILocator) => connector.sendMessage(ScriptMsg.ToggleDeleted, el),
toggleFilter: (payload: ClassFilterValue) =>
connector.sendMessage(ScriptMsg.ToggleFilter, payload),
toggleFilter: (payload: ClassFilterValue) => connector.sendMessage(ScriptMsg.ToggleFilter, payload),
toggleActiveGroup: (payload: ILocator[]) => connector.sendMessage(ScriptMsg.ToggleActiveGroup, payload),
unsetActive: (payload: ILocator | ILocator[]) => connector.sendMessage(ScriptMsg.UnsetActive, payload),
};
Expand Down
4 changes: 2 additions & 2 deletions src/pageServices/pageDataHandlers.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import connector, { sendMessage } from './connector';
import { request } from '../services/backend';
import { createOverlay } from './contentScripts/createOverlay';
import { getFullDocument } from '../common/utils/getFullDocument';
import { getFullDocumentWithStyles } from '../common/utils/getFullDocumentWithStyles';
// /* global chrome */

let overlayID;
Expand Down Expand Up @@ -36,7 +36,7 @@ And then tha data is sent to endpoint, according to selected library.
Function returns predicted elements. */
export const predictElements = (endpoint) => {
let pageData;
return Promise.all([sendMessage.getPageData(), getFullDocument()])
return Promise.all([sendMessage.getPageData(), getFullDocumentWithStyles()])
.then(([pageDataResult, documentResult]) => {
pageData = pageDataResult[0];
return sendToModel({ elements: pageData, document: documentResult }, endpoint);
Expand Down
Loading

0 comments on commit 275effa

Please sign in to comment.