javascriptgoogle-chrome-extensionbrowser-extension

How to set document.hidden to always return false?


I have a very specific need for some websites to always "believe" that their tab is active (as you'll see from the code below, it's mostly Google Maps and iCloud), equivalent to Chrome DevTools -> Rendering -> Emulate a focused page.

So far, changing a tab after the page has been loaded has been a success, but I need to refresh some tabs in the background using an extension - and those tabs return true for document.hidden, of course.

So far I've been looking for visibilitychange event and stopped propagating it, but since document.hidden is true on page load (in background), it doesn't matter. The HTML page I used as a test displays the results - if I load it in an active tab, it shows up as active and doesn't change the status; however, if I reload it using an auto-refresh extension, it shows as hidden.

I'm fairly new to extension development (though, thanks to this website I'm already using about half a dozen of my own extensions). Here is my test page:

<!DOCTYPE html>
<html>
<head>
<title>Visibility test</title>
</head>
<body>
<pre id="logger">
</pre>
<script>
var preContent = document.getElementById('logger').innerHTML;
var ts = new Date().toISOString();
if (document.hidden) {
    preContent = preContent + ts + " hidden" + "\n";
} else {
    preContent = preContent + ts + " visible" + "\n";
}
document.getElementById('logger').innerHTML = preContent;
document.addEventListener("visibilitychange", () => {
    var preContent = document.getElementById('logger').innerHTML;
    var ts = new Date().toISOString();
    if (document.hidden) {
        preContent = preContent + ts + " hidden" + "\n";
    } else {
        preContent = preContent + ts + " visible" + "\n";
    }
    document.getElementById('logger').innerHTML = preContent;
});
</script>
</body>
</html>

My extension:

manifest.json

{
    "manifest_version": 3,
    "name": "Always visible",
    "description": "BytE Always visible",
    "version": "1.4",
    "host_permissions": [
        "*://*/*"
    ],
    "permissions": [
        "activeTab",
        "tabs",
        "webRequest"
    ],

    "externally_connectable": {
        "matches": [
            "*://*.google.com/*",
            "*://*.icloud.com/*",
            "*://127.0.0.1/*"
        ]
    },
      
    "content_scripts": [
        {
            "matches": [
                "*://*.google.com/maps/*",
                "*://*.icloud.com/*",
                "*://127.0.0.1/*"
            ],
            "run_at": "document_start",
            "js": [
                "inject.js"
            ]
        }
    ],
    "web_accessible_resources": [
        {
            "resources": ["injected.js"],
            "matches": ["*://*.google.com/*","*://*.icloud.com/*","*://127.0.0.1/*"]
        }
    ]
}

inject.js

var s = document.createElement('script');
s.src = chrome.runtime.getURL('injected.js');
s.onload = function() {
    this.remove();
};
(document.head || document.documentElement).appendChild(s);

injected.js

for (event_name of ["visibilitychange", "webkitvisibilitychange", "blur"]) {
  window.addEventListener(event_name, function(event) {
        event.stopImmediatePropagation();
    }, true);
}

Solution

  • Simply run your script in the MAIN world at document_start as shown here and use Object.defineProperty to override document.hidden.

    1. Remove web_accessible_resources
    2. Remove inject.js
    3. Use injected.js and add "world": "MAIN" in content_scripts

    // manifest.json

      "content_scripts": [{
        "matches": [.........],
        "run_at": "document_start",
        "js": ["injected.js"]
        "world": "MAIN"
      }],
    

    // injected.js

    (() => {
      Object.defineProperty(Document.prototype, 'hidden', {get: () => false});
      
      for (const n of ['visibilitychange', 'webkitvisibilitychange', 'blur']) {
        window.addEventListener(n, stopEvent, true);
      }
    
      function stopEvent(evt) {
        evt.stopImmediatePropagation();
        evt.stopPropagation();
      }
    })();