0

My Chrome Extension is only working when I refresh the website I have it matched to. If I navigate the website, it will not successfully load, however, I do see the content-script being re-run in the console. It seems to fail because it's not finding the HTML elements I'm looking for. On refresh, it can find those HTML elements, and works fine.

I've been trying a few things like chrome.tabs.onUpdated.addListener and MutationObserver but couldn't figure it out. Most likely because my JS skills are fairly limited.

Link to the extension documents:
https://www.dropbox.com/s/x31uvkdpdcnhchz/chrome-ext-stack-example.zip?dl=0

How can I get the content-script to find the HTML elements as I navigate without always having to refresh?
Any thoughts on what I'm screwing up?
Thank you!

manifest.json

{
  "manifest_version": 3,
  "name": "Test",
  "description": "Example for StackOverflow",
  "version": "0.0.1",

  "host_permissions": ["<all_urls>"],
  "permissions": ["storage", "activeTab", "scripting", "tabs", "webNavigation"],

  "background": {
    "service_worker": "background.js"
  },

  "content_scripts": [
    {
      "matches": ["https://www.zillow.com/homedetails/*"],
      "js": ["ballpark.js"],
      "css": ["main.css"]
    }
  ],

  "web_accessible_resources": [
    {
      "resources": ["/images/*"],
      "matches": ["<all_urls>"]
    }
  ]
}

background.js

function injectScript(tabId) {
    chrome.scripting.executeScript(
        {
            target: {tabId: tabId},
            files: ['ballpark.js'],
        }
    );
}

// adds a listener to tab change
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {

    // check for a URL in the changeInfo parameter (url is only added when it is changed)
    if (changeInfo.url) {
        // calls the inject function
        injectScript(tabId);
    }
});

ballpark.js

var offerPrice;

// setTimeout(startWidget, 2000);

startWidget()

function startWidget() {
    if(location.href.match(/homedetails/g)) {
      console.log("YES Zillow Home Details URL");
      getAskingPrice();
      insertWidget();
  } else {
      console.log("NO Zillow Home Details URL");
  }
}

// Get Price from Zillow
function getAskingPrice() {
  var askingPrice = document.querySelector('[data-testid="price"] span');
  if(askingPrice !== null) {
    offerPrice = parseFloat(askingPrice.innerText.replace(/\$|,/g, ''));
    console.log(offerPrice + " Offer Price");
  } else {
    console.log("Null Asking Price");
  }
}

// Find Zillow widget to insert the extension widget
function insertWidget() {
  const select_div_for_bpd = document.querySelector('div.Spacer-c11n-8-65-2__sc-17suqs2-0');
  if(select_div_for_bpd !== null) {
    const ballpark_container = document.createElement("div");
    ballpark_container.setAttribute("id", "ballpark-container");
    select_div_for_bpd.appendChild(ballpark_container);

    ballpark_container.innerHTML = `
      <div class="ballpark-roi-container">
        <div><h1>${offerPrice}</h1> Offer Price</div>
      </div>
    `;
  } else {
    console.log("Cannot insert ballpark widget");
  }
}
benny
  • 85
  • 1
  • 11
  • for `getAskingPrice` and `insertWidget` you need to wait from the `DOM` to be ready first. – Dean Van Greunen Aug 25 '22 at 12:01
  • Tried `"run_at": "document_end"` but that didn't seem to make a difference. – benny Aug 25 '22 at 13:23
  • This site is a SPA. It doesn't load the page on navigation, it changes DOM and fakes the URL change. The solution is two-fold: remove `homedetails/` in `matches` and use MutationObserver to wait for the necessary element. See [this answer](https://stackoverflow.com/a/39508954) for more info. – wOxxOm Aug 25 '22 at 15:35
  • @wOxxOm Thanks! Got it going with [this post+video](https://openjavascript.info/2022/05/20/how-to-wait-until-an-element-exists-in-javascript/) that mirrored what I'm trying to do. I'm running into an issue where it's inserting the widget multiple times (~4) on the page but gonna keep trying to troubleshoot before asking for help. – benny Aug 28 '22 at 13:29
  • Add an id to your widget so you can check for its existence before adding. – wOxxOm Aug 28 '22 at 17:20
  • That def got me closer! However, it's adding my widget before it gets all of the data from the page. I've started [another thread here regarding this different issue](https://stackoverflow.com/questions/73520387/why-is-mutationobserver-inserting-my-chrome-extension-multiple-times-on-a-spa-we). – benny Aug 28 '22 at 19:56

3 Answers3

1

I added the following to the top of my content-script file and now my extension will keep looking for a specific class as I navigate the SPA website.

const observer = new MutationObserver(function() {
  if (document.getElementsByClassName('name-of-class-you-want')[0]) {
    // add what you want to do here...
  }
})

const target = document.querySelector('body');
const config = { childList: true };
observer.observe(target, config);
benny
  • 85
  • 1
  • 11
0

for getAskingPrice and insertWidget you need to wait from the DOM to be ready first.

try this

function startWidget() {
    if(location.href.match(/homedetails/g)) {
      console.log("YES Zillow Home Details URL");
      const fn = () => {
        getAskingPrice();
        insertWidget();
    }
    document.addEventListener('DOMContentLoaded', fn, false);
  } else {
      console.log("NO Zillow Home Details URL");
  }
}
Dean Van Greunen
  • 5,060
  • 2
  • 14
  • 28
  • 1
    Hmm. Looks like it's failing on the line `cost fn = () =>` It's no longer running the two functions: getAskingPrice, insertWidget. I'm probably doing something wrong. – benny Aug 25 '22 at 12:15
0

You should wait until the document completely loaded before you interact with the page.

You could implement a function which will return a promise if the document has loaded. Your function could look something like this.

function getCompletedReadyState()
{
    return new Promise((resolve) =>
    {
        if (document.readyState === 'complete')
        {
            resolve('completed');
        }
        document.onreadystatechange = () =>
        {
            if (document.readyState === 'complete')
            {
                resolve('completed');
            }
        };
    });
}

Then you could call the method at the end of your content script like so

...
(async () => {
   await getCompletedReadyState();
   startWidget()
})();

You can read more about document.readyState here

MrFabio_25
  • 478
  • 5
  • 9
  • I put the function getReadyState into a background.js file and the other line at the top of the content-script. I'm seeing an error in the Extensions panel: Uncaught SyntaxError: await is only valid in async functions and the top level bodies of modules – benny Aug 25 '22 at 12:28
  • I edited my post. Put it in your content script not background – MrFabio_25 Aug 25 '22 at 12:57
  • Thanks. Updated content-script start with following but not working till refresh. `(async () => { await getCompletedReadyState(); startWidget() })(); function getCompletedReadyState() { console.log("Ready state is running..."); return new Promise((resolve) => { if (document.readyState === 'complete') { resolve('completed'); } document.onreadystatechange = () => { if (document.readyState === 'complete') { resolve('completed'); } }; }); }` – benny Aug 25 '22 at 13:19
  • I'm not super at JS so I'm a little slow at picking things up. – benny Aug 25 '22 at 13:21
  • Put the function definitions above my code snippet ( marked with the "..." ) and make sure that you reload the extension after your changes here (chrome://extensions/) – MrFabio_25 Aug 25 '22 at 13:26
  • Thank you for the update! Placed the function `getCompletedReadyState()` at the top of content-script, then the `async`, then the rest of my code. Still only loading on refresh. I must be screwing something up. Here is [my implementation of that code](https://www.dropbox.com/sh/201xuh7ojky3cwh/AAAqfl4mhuFA-WhI7XCGHmsWa?dl=0). – benny Aug 27 '22 at 12:24
  • I'm also making sure to reload the extension and refresh the website. – benny Aug 27 '22 at 12:48