How to access react state of a page from chrome extension? - javascript

i am trying to write a chrome extension which could interact with the page which is a react-app. i am able to manipulate DOM by using popup.js.
Here is my popup.js
document.querySelector("#submit").addEventListener("click", async () => {
let [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
console.log(tab);
chrome.scripting.executeScript(
{
target: { tabId: tab.id },
files: ["scripts/script.js"],
},
(result) => {
console.log("injection result :", result);
}
);
});
by the help of script.js i may reach any sort of element except the reacts state.
I have found a Q/A over here which helps to find __reactInternal$ here is the code.
function FindReact(dom, traverseUp = 0) {
const key = Object.keys(dom).find((key) => {
return (
key.startsWith("__reactFiber$") || // react 17+
key.startsWith("__reactInternalInstance$")
); // react <17
});
const domFiber = dom[key];
if (domFiber == null) return key;
// react <16
if (domFiber._currentElement) {
let compFiber = domFiber._currentElement._owner;
for (let i = 0; i < traverseUp; i++) {
compFiber = compFiber._currentElement._owner;
}
return compFiber._instance;
}
// react 16+
const GetCompFiber = (fiber) => {
//return fiber._debugOwner; // this also works, but is __DEV__ only
let parentFiber = fiber.return;
while (typeof parentFiber.type == "string") {
parentFiber = parentFiber.return;
}
return parentFiber;
};
let compFiber = GetCompFiber(domFiber);
for (let i = 0; i < traverseUp; i++) {
compFiber = GetCompFiber(compFiber);
}
return compFiber.stateNode;
}
Even if the code above works well over the console, i cant use it in my script.js. what could be the reason? How can i access reacts state by the help of a chrome-extesion.
Note: i dont want to use reactdevtool. i am trying to write an end-user extension. Thanks in advance for your answers.

You can access it with
$r.state
I'll be adding the example visually.
To view state data this plugin will come in handy: https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi
example image

Related

How to change value of global var when it is being used as a parameter in a function? (Javascript)

I want to be able to change the value of a global variable when it is being used by a function as a parameter.
My javascript:
function playAudio(audioFile, canPlay) {
if (canPlay < 2 && audioFile.paused) {
canPlay = canPlay + 1;
audioFile.play();
} else {
if (canPlay >= 2) {
alert("This audio has already been played twice.");
} else {
alert("Please wait for the audio to finish playing.");
};
};
};
const btnPitch01 = document.getElementById("btnPitch01");
const audioFilePitch01 = new Audio("../aud/Pitch01.wav");
var canPlayPitch01 = 0;
btnPitch01.addEventListener("click", function() {
playAudio(audioFilePitch01, canPlayPitch01);
});
My HTML:
<body>
<button id="btnPitch01">Play Pitch01</button>
<button id="btnPitch02">Play Pitch02</button>
<script src="js/js-master.js"></script>
</body>
My scenario:
I'm building a Musical Aptitude Test for personal use that won't be hosted online. There are going to be hundreds of buttons each corresponding to their own audio files. Each audio file may only be played twice and no more than that. Buttons may not be pressed while their corresponding audio files are already playing.
All of that was working completely fine, until I optimised the function to use parameters. I know this would be good to avoid copy-pasting the same function hundreds of times, but it has broken the solution I used to prevent the audio from being played more than once. The "canPlayPitch01" variable, when it is being used as a parameter, no longer gets incremented, and therefore makes the [if (canPlay < 2)] useless.
How would I go about solving this? Even if it is bad coding practise, I would prefer to keep using the method I'm currently using, because I think it is a very logical one.
I'm a beginner and know very little, so please forgive any mistakes or poor coding practises. I welcome corrections and tips.
Thank you very much!
It's not possible, since variables are passed by value, not by reference. You should return the new value, and the caller should assign it to the variable.
function playAudio(audioFile, canPlay) {
if (canPlay < 2 && audioFile.paused) {
canPlay = canPlay + 1;
audioFile.play();
} else {
if (canPlay >= 2) {
alert("This audio has already been played twice.");
} else {
alert("Please wait for the audio to finish playing.");
};
};
return canPlay;
};
const btnPitch01 = document.getElementById("btnPitch01");
const audioFilePitch01 = new Audio("../aud/Pitch01.wav");
var canPlayPitch01 = 0;
btnPitch01.addEventListener("click", function() {
canPlayPitch01 = playAudio(audioFilePitch01, canPlayPitch01);
});
A little improvement of the data will fix the stated problem and probably have quite a few side benefits elsewhere in the code.
Your data looks like this:
const btnPitch01 = document.getElementById("btnPitch01");
const audioFilePitch01 = new Audio("../aud/Pitch01.wav");
var canPlayPitch01 = 0;
// and, judging by the naming used, there's probably more like this:
const btnPitch02 = document.getElementById("btnPitch02");
const audioFilePitch02 = new Audio("../aud/Pitch02.wav");
var canPlayPitch02 = 0;
// and so on
Now consider that global data looking like this:
const model = {
btnPitch01: {
canPlay: 0,
el: document.getElementById("btnPitch01"),
audioFile: new Audio("../aud/Pitch01.wav")
},
btnPitch02: { /* and so on */ }
}
Your event listener(s) can say:
btnPitch01.addEventListener("click", function(event) {
// notice how (if this is all that's done here) we can shrink this even further later
playAudio(event);
});
And your playAudio function can have a side-effect on the data:
function playAudio(event) {
// here's how we get from the button to the model item
const item = model[event.target.id];
if (item.canPlay < 2 && item.audioFile.paused) {
item.canPlay++;
item.audioFile.play();
} else {
if (item.canPlay >= 2) {
alert("This audio has already been played twice.");
} else {
alert("Please wait for the audio to finish playing.");
};
};
};
Side note: the model can probably be built in code...
// you can automate this even more using String padStart() on 1,2,3...
const baseIds = [ '01', '02', ... ];
const model = Object.fromEntries(
baseIds.map(baseId => {
const id = `btnPitch${baseId}`;
const value = {
canPlay: 0,
el: document.getElementById(id),
audioFile: new Audio(`../aud/Pitch${baseId}.wav`)
}
return [id, value];
})
);
// you can build the event listeners in a loop, too
// (or in the loop above)
Object.values(model).forEach(value => {
value.el.addEventListener("click", playAudio)
})
below is an example of the function.
btnPitch01.addEventListener("click", function() {
if ( this.dataset.numberOfPlays >= this.dataset.allowedNumberOfPlays ) return;
playAudio(audioFilePitch01, canPlayPitch01);
this.dataset.numberOfPlays++;
});
you would want to select all of your buttons and assign this to them after your html is loaded.
https://developer.mozilla.org/en-US/docs/Web/API/Document/getElementsByClassName
const listOfButtons = document.getElementsByClassName('pitchButton');
listOfButtons.forEach( item => {
item.addEventListener("click", () => {
if ( this.dataset.numberOfPlays >= this.dataset.allowedNumberOfPlays ) return;
playAudio("audioFilePitch" + this.id);
this.dataset.numberOfPlays++;
});

Function declared in a loop contains unsafe references to variable(s)

My code checks if a user is available. See snippet below:
const users = ['user1', 'user2', 'user3', 'user4']
const usersToAdd = 2
const getRandomWorker = (userArray) => {
return userArray[Math.floor(userArray.length * Math.random())]
}
const availableUsers = []
for (let i = 0; i < usersToAdd; i += i) {
let randomWorker = getRandomWorker(users)
let didAddWorker = false
while (!didAddWorker) {
if (checkIfUserAvailable(randomWorker)) {
availableUsers.push(randomWorker)
users = users.filter((user) => user !== randomWorker)
didAddWorker = true
} else if (!users.length) {
didAddWorker = true
} else {
users = users.filter((user) => user !== randomWorker)
}
}
}
My only problem is that it contains unsafe references to variables(s) because I get the following error:
Function declared in a loop contains unsafe references to variable(s) randomWorker.
I've searched around and fiddled with my code but I can't get rid of the error. I don't know where to look anymore.
As T.J Crowder suggested I had to make a function outside of the loop:
const filterOut = (array, target) => array.filter(element => element !== target);
That fixed it. A bit strange but I can continue know. Thanks!

React Child Component Is Not Rerendering When Props Are Updated

My parent component takes input from a form and the state changes when the value goes out of focus via onBlur.
useEffect(() => {
let duplicate = false;
const findHierarchy = () => {
duplicationSearchParam
.filter(
(object, index) =>
index ===
duplicationSearchParam.findIndex(
(obj) => JSON.stringify(obj.name) === JSON.stringify(object.name)
)
)
.map((element) => {
DuplicateChecker(element.name).then((data) => {
if (data.status > 200) {
element.hierarchy = [];
} else {
element.hierarchy = data;
}
});
if (duplicate) {
} else {
duplicate = element?.hierarchy?.length !== 0;
}
});
return duplicate;
};
let dupe = findHierarchy();
if (dupe) {
setConfirmationProps({
retrievedData: formData,
duplicate: true,
responseHierarchy: [...duplicationSearchParam],
});
} else {
setConfirmationProps({
retrievedData: formData,
duplicate: false,
responseHierarchy: [],
});
}
}, [duplicationSearchParam]);
I have a child component also uses a useeffect hook to check for any state changes of the confirmationProps prop.
the issue is that the event gets triggered onblur, and if the user clicks on the next button. this function gets processes
const next = (data) => {
if (inProgress === true) {
return;
}
inProgress = true;
let countryLabels = [];
formData.addresses?.map((address) => {
fetch(`/api/ref/country/${address?.country}`)
.then((data) => {
countryLabels.push(data.label);
return countryLabels;
})
.then((countries) => {
let clean = MapCleanse(data, countries);
fetch("/api/v1/organization/cleanse", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(clean),
})
.then((data) => {
if (data.status > 200) {
console.log(data.message);
message.error(getErrorCode(data.message.toString()));
} else {
Promise.all([confirmationProps, duplicationSearchParam]).then(
(values) => {
console.log(values);
console.log(data);
setCleansed(data);
**setCurrent(current + 1);**
inProgress = false;
}
);
}
})
.catch((err) => {
console.log(err);
inProgress = false;
});
})
.catch((err) => {
console.log(err);
inProgress = false;
});
});
console.log(confirmationProps);
};
The important part in the above code snippet is the setCurrent(current + 1) as this is what directs our code to render the child component
in the child component, i have a use effect hook that is watching [props.duplicateData.responseHierarchy]
I do output the values of props.duplicateData.responsehierarchy to the console to see if the updated information gets passed to the child component and it does. the values are present.
I have a conditional render statement that looks like this
{cleansedTree?.length > 0 || treeDuplicate ? (...)}
so although the data is present and is processed and massaged in the child component. it still will not re render or display properly. unless the user goes back to the previous screen and proceeds to the next screen again... which forces a re-render of the child component.
I have boiled it down and am assuming that the conditional rendering of the HTML is to blame. Or maybe when the promise resolves and the state gets set for the confirmation props that the data somehow gets lost or the useefect doesn't pick it up.
I have tried the useefect dependency array to contain the props object itself and other properties that arent directly related
UPDATE: this is a code snippet of the processing that gets done in the childs useeffect
useEffect(() => {
console.log(props.duplicate);
console.log(props.duplicateData);
console.log(props.confirmationProps);
let newArray = props.duplicateData.filter((value) => value);
let duplicateCheck = newArray.map((checker) =>
checker?.hierarchy?.find((Qstring) =>
Qstring?.highlightedId?.includes(UUIDToString(props?.rawEdit?.id))
)
);
duplicateCheck = duplicateCheck.filter((value) => value);
console.log(newArray, "new array");
console.log(duplicateCheck, "duplicate check");
if (newArray?.length > 0 && duplicateCheck?.length === 0) {
let list = [];
newArray.map((dupeData) => {
if (dupeData !== []) {
let clean = dupeData.hierarchy?.filter(
(hierarchy) => !hierarchy.queryString
);
let queryParam = dupeData.hierarchy?.filter(
(hierarchy) => hierarchy.queryString
);
setSelectedKeys([queryParam?.[0]?.highlightedId]);
let treeNode = {};
if (clean?.length > 0) {
console.log("clean", clean);
Object.keys(clean).map(function (key) {
treeNode = buildDuplicate(clean[key]);
list.push(treeNode);
return list;
});
setCleansedTree([...list]);
setTreeDuplicate(true);
} else {
setTreeDuplicate(false);
}
}
});
}
}, [props.duplicateData.responseHierarchy]);
This is a decently complex bit of code to noodle through, but you did say that **setCurrent(current + 1);** is quite important. This pattern isn't effectively handling state the way you think it is...
setCurrent(prevCurrent => prevCurrent + 1)
if you did this
(count === 3)
setCount(count + 1) 4
setCount(count + 1) 4
setCount(count + 1) 4
You'd think you'd be manipulating count 3 times, but you wouldn't.
Not saying this is your answer, but this is a quick test to see if anything changes.
The issue with this problem was that the state was getting set before the promise was resolved. to solve this issue I added a promise.all function inside of my map and then proceeded to set the state.
What was confusing me was that in the console it was displaying the data as it should. but in fact, as I learned, the console isn't as reliable as you think. if someone runs into a similar issue make sure to console the object by getting the keys. this will return the true state of the object, and solve a lot of headache

How to get all events on all DOM elements of a page visited by Puppeteer - basically getEventListeners

I am working on some Puppeteer powered website analytics and would really need to list all events on a page.
It's easy with "normal" JavaScript, so I thought I could just evaluate it in the Puppeteer and get to other tasks.
Well - it is not so easy and "getEventListeners" is not working for example. So this code below is not working (but if I take the code that gets valuated, copy it to browser's console and run - it works well);
exports.getDomEventsOnElements = function (page) {
return new Promise(async (resolve, reject) => {
try {
let events = await page.evaluate(() => {
let eventsInDom = [];
const elems = document.querySelectorAll('*');
for(i = 0; i < elems.length; i++){
const element = elems[i];
const allEventsPerEl = getEventListeners(element);
if(allEventsPerEl){
const filteredEvents = Object.keys(allEventsPerEl).map(function(k) {
return { event: k, listeners: allEventsPerEl[k] };
})
if(filteredEvents.length > 0){
eventsInDom.push({
el: element,
ev: filteredEvents
})
}
}
}
return eventsInDom;
})
resolve(events);
} catch (e) {
reject(e);
}
})
}
I've investigated further and it looks like this will not work in Puppeteer and even tried with good old JQuery's const events = $._data( element[0], 'events' ); but it does not work either.
Then I stumbled upon Chrome DevTools Protocol (CDP) and there it should be possible to get it by defining a single element on beforehand;
const cdp = await page.target().createCDPSession();
const INCLUDE_FN = true;
const { result: {objectId} } = await cdp.send('Runtime.evaluate', {
expression: 'foo',
objectGroup: INCLUDE_FN ?
'provided' : // will include fn
'' // wont include fn
});
const listeners = await cdp.send('DOMDebugger.getEventListeners', { objectId });
console.dir(listeners, { depth: null });
(src: https://github.com/puppeteer/puppeteer/issues/3349)
But this looks too complicated when I would like to check each and every DOM element for events and add them to an array. I suspect there is a better way than looping the page elements and running CDP for each and every one. Or better said - I hope :)
Any ideas?
I would just like to have an array of all elements with (JS) events like for example:
let allEventsOnThePage : [
{el: "blutton", events : ["click"]},
{el: "input", events : ["click", "blur", "focus"]},
/* you get the picture */
];
I was curious so I looked into expanding on that CDP example you found, and came up with this:
async function describe (session, selector = '*') {
// Unique value to allow easy resource cleanup
const objectGroup = 'dc24d2b3-f5ec-4273-a5c8-1459b5c78ca0';
// Evaluate query selector in the browser
const { result: { objectId } } = await session.send('Runtime.evaluate', {
expression: `document.querySelectorAll("${selector}")`,
objectGroup
});
// Using the returned remote object ID, actually get the list of descriptors
const { result } = await session.send('Runtime.getProperties', { objectId });
// Filter out functions and anything that isn't a node
const descriptors = result
.filter(x => x.value !== undefined)
.filter(x => x.value.objectId !== undefined)
.filter(x => x.value.className !== 'Function');
const elements = [];
for (const descriptor of descriptors) {
const objectId = descriptor.value.objectId;
// Add the event listeners, and description of the node (for attributes)
Object.assign(descriptor, await session.send('DOMDebugger.getEventListeners', { objectId }));
Object.assign(descriptor, await session.send('DOM.describeNode', { objectId }));
elements.push(descriptor);
}
// Clean up after ourselves
await session.send('Runtime.releaseObjectGroup', { objectGroup });
return elements;
}
It will return an array of objects, each with (at least) node and listeners attributes, and can be used as follows:
/** Helper function to turn a flat array of key/value pairs into an object */
function parseAttributes (array) {
const result = [];
for (let i = 0; i < array.length; i += 2) {
result.push(array.slice(i, i + 2));
}
return Object.fromEntries(result);
}
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://chromedevtools.github.io/devtools-protocol', { waitUntil: 'networkidle0' });
const session = await page.target().createCDPSession();
const result = await describe(session);
for (const { node: { localName, attributes }, listeners } of result) {
if (listeners.length === 0) { continue; }
const { id, class: _class } = parseAttributes(attributes);
let descriptor = localName;
if (id !== undefined) { descriptor += `#${id}`; }
if (_class !== undefined) { descriptor += `.${_class}`; }
console.log(`${descriptor}:`);
for (const { type, handler: { description } } of listeners) {
console.log(` ${type}: ${description}`);
}
}
await browser.close();
})();
which will return something like:
button.aside-close-button:
click: function W(){I.classList.contains("shown")&&(I.classList.remove("shown"),P.focus())}
main:
click: function W(){I.classList.contains("shown")&&(I.classList.remove("shown"),P.focus())}
button.menu-link:
click: e=>{e.stopPropagation(),I.addEventListener("transitionend",()=>{O.focus()},{once:!0}),I.classList.add("shown")}

How to check if image exists?

I have application where images should be set in some of the pages. These images are on the server and each image has unique ID appended. Here is example of the image names:
AA_image1.jpg
AB_image1.jpg
AC_image1.jpg
AD_image1.jpg
AE_image1.jpg
I seen what previous developers did and how they checked the image existance. They used JSON file that has id and image name. Here is example of that code:
var images = [{
"id": "AA",
"image": "AA_image1.jpg"
},
{
"id": "AB",
"image": "AB_image1.jpg"
},
{
"id": "AC",
"image": "AC_image1.jpg"
},
{
"id": "AD",
"image": "AD_image1.jpg"
},
{
"id": "AE",
"image": "AE_image1.jpg"
}
];
var imgID = "AC";
var imgPrimary = "AC_image1.jpg";
var found = false;
var imgDefault = "default.jpg";
for (i = 1; i < images.length; i++) {
if (images[i].id == imgID && (images[i].image).toLowerCase() == (imgID + '_image1.jpg').toLowerCase()) {
found = true;
break;
}
}
if (found === true) {
console.log(imgPrimary);
} else {
console.log(imgDefault);
}
The example above seems pretty simple but my concern is what if image get removed from the folder and JSON file is not updated? In that case we would load the image that do not exist instead of default image. I'm wondering if this approach would be better:
var imgID = "AC";
var imgNames = [imgID + '_image1','default'];
var imgResults = {};
for(var i = 0; i < imgNames.length; i++){
checkImage( imgNames[i] );
}
function checkImage( imgName, keyName ) {
$.ajax({
type: "GET",
async: true,
url: "images/"+imgName+".jpg",
}).done(function(message,text,jqXHR){
imgResults[imgName] = true;
}).fail(function(jqXHR, textStatus, errorThrown){
imgResults[imgName] = false;
});
}
Here is example of imgResults after the process was completed:
console.log(imgResults);
Console result:
{
"default": true,
"AG_image1": false
}
The only problem I'm experiencing with the second example is that if I try to check the result based on the key I'm getting undefined. Here is example:
console.log(imgResults["AG_image1"]);
This is result in the console:
undefined
Between the two I'm not sure which one is better and more secure. If anyone have suggestions please let me know.
This is probably the shortest possible code to handle lost images in JS. imagePad is a reference to the DOM element in which you want to show the image.
var img = new Image();
img.addEventListener('error', function (e) {
// Image not found, show the default
this.src = iconBase + 'gen.png';
});
img.addEventListener('load', function () {
imagePad.appendChild(img);
}
img.src = 'the_given_src';
AJAX request are asynchronous by default
The reason that you're unable to search the imgResults object when you are trying is because AJAX requests are asynchronous, and the request has not completed when you're trying to access the results. You need to wait for the request to complete before continuing.
The reason that console.log shows the results is because console.log is lazy and doesn't evaluate the object until you expand it in dev tools, console.dir will show you an empty object at that point.
Furthermore, as you have multiple requests that you want to wait for, you'll want to create an array of promises where each promise corresponds to the load/failure of each request, then use Promise.all to wait for all the promises to complete before operating on the results.
Verifying that a resource exists
To verify that a resource exists, you can use a HEAD request instead of a GET request, such as to prevent loading the entire image for no reason. There's also no need to depend on a massive library such as jQuery for AJAX requests as XMLHttpRequest is very well supported in this day and age.
class ResourceValidator extends EventTarget {
constructor(target) {
super()
this.target = target
this._ready = false
this._valid = false
this.validate()
}
validate() {
const request = new XMLHttpRequest()
request.addEventListener('load', event => {
this._ready = true
this._valid = true
this.dispatchEvent(new Event('ready'))
})
request.addEventListener('error', event => {
this._ready = true
this.dispatchEvent(new Event('ready'))
})
request.open('HEAD', this.target)
request.send()
}
get ready() {
return new Promise(resolve => {
if(this._ready === true) resolve(true)
else this.addEventListener('ready', _ => resolve(true))
})
}
get valid() {
return new Promise(resolve => {
if(this._ready === true) resolve(this._valid)
else this.addEventListener('ready', _ => resolve(this._valid))
})
}
}
async function validateImageSources(sources) {
const results = {}
const promises = []
for(let source of sources) {
const validator = new ResourceValidator(source)
const promise = validator.valid
promise.then(valid => results[source] = valid)
promises.push(promise)
}
await Promise.all(promises)
return results
}
validateImageSources([
'https://picsum.photos/200',
'https://nosuchaddress.io/image.png'
]).then(results => {
console.log(results)
console.log(results['https://picsum.photos/200'])
})

Categories