Override Element.prototype.attachShadow using Chrome Extension
Asked Answered
C

2

6

I need to access the DOM of a web component that has a closed Shadow DOM for some Selenium testing. I've read in several references that you can override Element.prototype.attachShadow on the document startup in order to change the Shadow from closed to open. In order to do this, I created a Chrome Extension. Below is my manifest.json:

{
    "name": "SeleniumTesting",
    "description": "Extension to open closed Shadow DOM for selenium testing",
    "version": "1",
    "author": "SeleniumTesting",
    "manifest_version": 2,
    "permissions": ["downloads", "<all_urls>"],
    "content_scripts": [{
        "matches": ["http://localhost:5000/*"],
        "run_at": "document_start",
        "all_frames": true,
        "js": ["shadowInject.js"]
    }]
}

And my shadowInject.js

console.log('test');
Element.prototype._attachShadow = Element.prototype.attachShadow;
Element.prototype.attachShadow = function () {
    console.log('attachShadow');
    return this._attachShadow( { mode: "open" } );
};

In order to test it, I created my component in an ASPNetCore MVC project. Below is my javascript that creates the custom component:

customElements.define('x-element', class extends HTMLElement {
    constructor() {
        super(); 
        this._shadowRoot = this.attachShadow({
            mode: 'closed'
        });
        this._shadowRoot.innerHTML = `<div class="wrapper">
        <a href="download/File.pdf" download>
            <button id="download">Download File</button>
        </a>
        <p>Link para download</p>
      </div>`;
    }
});

And my HTML file that uses it:

@page
@model IndexModel
@{
    ViewData["Title"] = "Home page";
}
<script src='~/js/componente.js'></script>

<div class="text-center">
    <h1 class="display-4">Welcome</h1>
    <p>Learn about <a href="https://learn.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>
    <x-element id='comp'></x-element>
</div>

I load my extension into Chrome, and I run the page. I get the console log Test, but the attachShadow method is never called, and I still cannot access the closed shadow DOM

Console log

I would really appreciate some help on what I`m doing wrong. Thank you very much.

FINAL SOLUTION

After applying the changes in the answer, I need to make some adjustments to the manifest.json. Below is the final version:

{
    "name": "SeleniumTesting",
    "description": "Extension to open closed Shadow DOM for selenium testing",
    "version": "1",
    "author": "SeleniumTesting",
    "manifest_version": 2,
    "permissions": ["downloads", "<all_urls>"],
    "content_scripts": [{
        "matches": ["http://localhost:5000/*"],
        "run_at": "document_start",
        "all_frames": true,
        "js": ["shadowInject.js"]
    }],
    "web_accessible_resources": ["injected.js"]
}

Now it worked, and the Shadow DOM changed to open enter image description here

Calandra answered 2/3, 2019 at 1:24 Comment(0)
S
10

You should not put the code in content_scripts because content_scripts is not the same as the current page context.

You try to change the shadowInject.js code to:

const injectedScript = document.createElement('script');
injectedScript.src = chrome.extension.getURL('injected.js');
(document.head || document.documentElement).appendChild(injectedScript);

Then create a injected.js file in the same directory:

The file content is:

console.log('test');
Element.prototype._attachShadow = Element.prototype.attachShadow;
Element.prototype.attachShadow = function () {
    console.log('attachShadow');
    return this._attachShadow( { mode: "open" } );
};

You can try it. If there is any problem, please let me know

Shading answered 2/3, 2019 at 1:57 Comment(4)
I'm gonna try. How should I change my manifest.json to fire to shadowInject.js, instead of content_script?Calandra
manifest.json does not need to be changed. You just need to change the contents of the shadowInject.js file and create an injected.js file.Shading
Actually, I needed to add "web_accessible_resources": ["injected.js"] so that the injected.js could be recognised and used by get getURL, but a simple google led me to this. I've posted the final manifest.json to help anyone in the future. Thank you very much, again, for your helpCalandra
Once you manage to injected script to hook attachShadow method, how to you do to make content scripts to communicate with injected script, please? Do you use combination of dispatchEvent and addEventListener ? Or do you use a common object to trigger process while an event raises ? Thanks in advance for your answer,Capping
D
3

As mentioned in Black-Hole's answer, content scripts have their own versions of the DOM objects so you'll have to inject an additional script to run in the real DOM.

If you want to touch the page as little as possible and keep shadows closed to the rest of the page you can use this script:

{
  const shadows = new WeakMap();
  const original = Element.prototype.attachShadow;
  Element.prototype.attachShadow = function() {
    const shadow = original.apply(this, arguments);
    shadows.set(this, shadow);
    return shadow;
  };

  // Later...
  shadows.get(document.querySelector("x-element"));
}
Disorder answered 4/3, 2019 at 14:5 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.