How to track DOM change in chrome extension? - google-chrome-extension

I am writing a chrome extension where I want to fetch all the images exist on a page but some of the images load after some time (may be through ajax) which I could not fetch once the DOM is idle. Is there any way to track the DOM change after the page is loaded?

Updated for 2020:
The recommended way nowadays is to use the Mutation Observer API.
let observer = new MutationObserver(mutations => {
for(let mutation of mutations) {
for(let addedNode of mutation.addedNodes) {
if (addedNode.nodeName === "IMG") {
console.log("Inserted image", addedNode);
}
}
}
});
observer.observe(document, { childList: true, subtree: true });

You can use document.addEventListener with the DOMNodeInserted event. Your callback will have to check each node insertion to see if it is the type of node you are looking for. Something like the following should work.
function nodeInsertedCallback(event) {
console.log(event);
};
document.addEventListener('DOMNodeInserted', nodeInsertedCallback);

Related

how to add an event listener in connectCallback

I want to wait until elements are rendered in the dom to dispatch an event. I have a lit element that is wrapped around a react element.
In the connectedCallback I have the following
connectedCallback() {
super.connectedCallback();
CommentsManager.register(this);
const event = new Event('ccx-comments-loaded');
window.dispatchEvent(event);
}
in the constructor, I have the following
this.isReadyPromise = new Promise(function(resolve, reject) {
window.addEventListener('ccx-comments-loaded', () => {
resolve(true);
});
});
How can I remove the listener that I created?
I want to wait until elements are rendered in the dom to dispatch an
event.
This looks like you could use an already existing updateComplete method from lit-element lifecycle. It is executed after render and it sounds like you may want to use it instead of having your own events.
You could read more about it here:
https://lit.dev/docs/v1/components/lifecycle/#updatecomplete
This would be a clean and more straightforward way to use lit-element. This way you don't reinvent something existing and your code would be more straightforward and clear for the other developers.
Store a reference to the Event Listener, then remove it in the disconnectedCallback
customElements.define("my-element", class extends HTMLElement {
constructor() {
super();
this.listener = window.addEventListener("click", () => {
this.remove();
});
}
connectedCallback() {
this.innerHTML = `Listening! Click to remove Web Component`;
}
disconnectedCallback() {
// element is no longer in the DOM; 'this' scope still available!!!
window.removeEventListener("click", this.listener);
document.body.append("Removed Web Component and Listener");
}
})
<my-element></my-element>

Chrome Extension API Calls order and DOM Information

I'm working on an extension that is supposed to extract information from the DOM based specific classes/tags,etc, then allow the user to save the information as a CSV file.
I'm getting stuck on a couple of places and haven't been able to find answers to questions similar enough.
Where I am tripped up at is:
1) Making sure that the page has completely loaded so the chrome.tabs.query doesn't return null a couple of times before the promise actually succeeds and allows the blocksF to successfully inject. I have tried placing it within a settimeout function but the chrome api doesn't seem to work within such the function.
2) Saving the extracted information so when the user moves onto a new page, the information is still there. I'm not sure if I should use the chrome.storage api call or simply save the information as an array and keep passing it through. It's just text, so I don't believe that it should take up too much space.
Then main function of the background.js is below.
let mainfunc = chrome.tabs.onUpdated.addListener(
async(id, tab) => {
if (buttonOn == true) {
let actTab = await chrome.tabs.query({
active: true,
currentWindow: true,
status: "complete"
}).catch(console.log(console.error()));
if (!actTab) {
console.log("Could not get URL. Turn extension off and on again.");
} else {
console.log("Tab information recieved.")
};
console.log(actTab);
let blocksF = chrome.scripting.executeScript({
target: { tabId: actTab[0]['id'] },
func: createBlocks
})
.catch(console.error)
if (!blocksF) {
console.log("Something went wrong.")
} else {
console.log("Buttons have been created.")
};
/*
Adds listeners and should return value of the works array if the user chose to get the information
*/
let listenersF = chrome.scripting.executeScript({
target: { tabId: actTab[0]['id'] },
func: loadListeners
})
.catch(console.error)
if (!listenersF) {
console.log("Listeners failed to load.")
} else {
console.log("Listeners loaded successfully.")
};
console.log(listenersF)
};
});
Information from the DOM is extracted through an event listener on a div/button that is added. The event listener is added within the loadListeners function.
let workArr = document.getElementById("getInfo").addEventListener("click", () => {
let domAr = Array.from(
document.querySelectorAll(<class 1>, <class 2>),
el => {
return el.textContent
}
);
let newAr = []
for (let i = 0; i < domAr.length; i++) {
if (i % 2 == 0) {
newAr.push([domAr[i], domAr[i + 1]])
}
}
newAr.forEach((work, i) => {
let table = document.getElementById('extTable');
let row = document.createElement("tr");
row.appendChild(document.createElement("td")).textContent = work[0];
row.appendChild(document.createElement("td")).textContent = work[1];
table.appendChild(row);
});
return newAr
I've been stuck on this for a couple of weeks now. Any help would be appreciated. Thank you!
There are several issues.
chrome methods return a Promise in MV3 so you need to await it or chain on it via then.
tabs.onUpdated listener's parameters are different. The second one is a change info which you can check for status instead of polling the active tab, moreover the update may happen while the tab is inactive.
catch(console.log(console.error())) doesn't do anything useful because it immediately calls these two functions so it's equivalent to catch(undefined)
Using return newArr inside a DOM event listener doesn't do anything useful because the caller of this listener is the internal DOM event dispatcher which doesn't use the returned value. Instead, your injected func should return a Promise and call resolve inside the listener when done. This requires Chrome 98 which added support for resolving Promise returned by the injected function.
chrome.tabs.onUpdated.addListener(onTabUpdated);
async function onTabUpdated(tabId, info, tab) {
if (info.status === 'complete' &&
/^https?:\/\/(www\.)?example\.com\//.test(tab.url) &&
await exec(tabId, createBlocks)) {
const [{result}] = await exec(tabId, loadListeners);
console.log(result);
// here you can save it in chrome.storage if necessary
}
}
function exec(tabId, func) {
// console.error returns `undefined` so we don't need try/catch,
// because executeScript is always an array of objects on success
return chrome.scripting.executeScript({target: {tabId}, func})
.catch(console.error);
}
function loadListeners() {
return new Promise(resolve => {
document.getElementById('getInfo').addEventListener('click', () => {
const result = [];
// ...add items to result
resolve(result);
});
});
}

Chrome extension background script sometimes does not run after install or update

I have had recent reports of a chrome extension that I develop that stops working after an update or a fresh install. The background script seems to not start at all.
There is no response to messages sent to it from the content scripts.
There is no process for it in the task manager.
Opening background page from chrome://extensions does not show any activity in the console, or show any source files.
Profiling, memory snapshot buttons are disabled.
Once this issue appears, it persists for the chrome profile even after reloading or uninstalling/reinstalling the extension.
Restarting chrome resolves the problem.
The issue has been seen on chrome v79. But I cannot say for sure that it is exclusive to this version, as the issue is difficult to reproduce and seemingly random.
Has anyone seen such an issue, or has any ideas what to look for? I am happy to update my question with any new info I have or with any info you need.
Edit:
Here is my webNavigation listener, which is used to inject content scripts. This handler is wired up in the 'root' context of the background script (not asynchronously inside an event handler)
chrome.webNavigation.onCompleted.addListener((details) ⇒ {
if(details.frameId === 0) {
injectScript(
'js/contentScript.js',
details.tabId,
details.frameId,
details.url
).catch((e) ⇒ {});
}
}
The injectScript function is as follows
export const injectScript = ƒ (scriptPath,tab,frame,tabUrl) {
return new Promise((res,rej) ⇒ {
let options = {
file : scriptPath,
allFrames : false,
frameId : frame,
matchAboutBlank: false,
runAt : 'document_idle',
};
const cb = ƒ () {
if (chrome.runtime.lastError) {
let err = new Error('Could not inject script');
capture(err,{
...options,
tabUrl,
lastError : chrome.runtime.lastError.message,
});
rej(err);
}else{
res();
}
};
if (tabUrl.indexOf('.salesforce.com') !== -1) {
window.setTimeout(() => {
chrome.tabs.executeScript(tab,options,cb);
},500);
}else{
chrome.tabs.executeScript(tab,options,cb);
}
});
};
Note above, the capture function reports the error to a backend and I cannot see it being reported there as well. Cannot add a breakpoint in code because no source appears in the background page, as noted above.
A background service worker is loaded when it is needed, and unloaded when it goes idle.
https://developer.chrome.com/docs/extensions/mv3/service_workers/
You can use the following methods:
// Keep heartbeat
let heartTimer;
const keepAlive = () => {
heartTimer && clearTimeout(heartTimer);
heartTimer = setTimeout(() => {
chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) {
console.info('[heartbeat]')
tabs.length && chrome.tabs.sendMessage(
tabs[0].id,
{ action: "heartbeat" }
);
});
keepAlive();
}, 10000);
};
keepAlive();

Chrome WebRequest API: How to block "Set-Cookies"?

I need to block all cookies from a certain domain. (I cant use the content settings API, since FireFox doesn't support it yet)
I am not having a whole lot of success with that I have now, I wonder if I am going in the right direction?
Using the WebRequest API, I added a listener to
onHeadersReceived
and made sure my function returned a promise, I then went through the headers like so:
function modifyHeaders(headers: HttpHeader[]) {
headers.forEach((header: HttpHeader) => {
if (header.name === "set-cookie") {
/* makeExpire sets "Expires= sometimeInThePast" */
header.value = makeExpire(header.value!)
}
})
}
modifyHeaders(headers);
return { responseHeaders: headers }
This seems like the lowest overhead way of doing it, but so far it doesn't seem to work. I think I might be on the wrong track some how.
function onHeadersReceived(details) {
if (details.responseHeaders) {
return {
responseHeaders: details.responseHeaders.filter((x) => {
return x.name.toLowerCase() !== 'set-cookie';
})
};
}
return {};
}
browser.webRequest.onHeadersReceived.addListener(onHeadersReceived, { urls: ["<all_urls>"] }, ["responseHeaders", "blocking"]);

Refresh page while element is not presented with cucumber-nightwatch (selenium web driver)

I need to refresh page while element is not presented
i'm trying something like this, but it doesn't help
When(/^"([^"]*)" task status changed$/, taskName => {
let needRefresh = true;
do {
client.url(`${client.globals.env.url}${client.globals.env.index}/messaging/messages`)
.pause(10000)
.getTagName(`//div[contains(#class, "task-checkbox")]//*[contains(text(), "${taskName}")]`, res => {
client.equal(res.value, 'div')
}).pause(20000);
} while (!needRefresh)
});
how to do it correctly?
To be able to use complex asynchronous operations I suggest to use the new async functions. If your NodeJs version does not support it natively I suggest to use Babel. There is an example for that in the nightwatch-cucumber example folder. To be able to refresh the page until some condition you can use the following example.
When(/^"([^"]*)" task status changed$/, async (taskName) => {
let needRefresh = true;
do {
await client.refresh();
await client.pause(10000);
needRefresh = await client.getTagName(...
} while (!needRefresh)
});

Resources