skip to Main Content

I have a local testing environment, where I want to temporary override querySelector. I know that monkeypatching is bad practice, but in this case this code will only be used locally on developer side. I have this snippet that I wrote (overrides querySelector to fetch all selectors with another substring in the selector called addonID):

  function maybeOverrideForTestStart(partialIDOfWidget, fullIDOfWidget) {
    if(!isLocal) return;
    const addonID = fullIDOfWidget.replace(partialIDOfWidget, "");
    Element.prototype.querySelectorTemp = Element.prototype.querySelector.bind(Element);
    
    Element.prototype.querySelector = function(selector) {
      const element = this.querySelectorTemp(selector);
      if (element) return element;
      if (addonID) {
        return this.querySelectorTemp(selector + addonID) || null;
      }
    };

  }
  function maybeOverrideForTestEnd() {
    if(!isLocal) return;
    Element.prototype.querySelector = Element.querySelectorTemp;
  }

I call maybeOverrideForTestStart in the beginning of my testing, and maybeOverrideForTestEnd in the end. But this doesn’t work, and I’m not sure what I’m missing. I’m getting either someElement.querySelector is not a function or "Uncaught TypeError: Illegal invocation".

Note – I also cannot understand if this also overrides the document.querySelector and document.body.querySelector or just someElement.querySelector.

Help is appreciated, thanks.

2

Answers


  1. I’m not sure if you need to .bind() the function, but you could keep a reference to the original function in a constant, for example

    const querySelector = Element.prototype.querySelector;
    
    function maybeOverrideForTestStart(partialIDOfWidget, fullIDOfWidget) {
      // ....
    
      Element.prototype.querySelector = function (selector) {
        const element = querySelector(selector);
        if (element) return element;
        if (addonID) {
          return querySelectorTemp(selector + addonID) || null;
        }
      };
    }
    
    function maybeOverrideForTestEnd() {
      // ...
      Element.prototype.querySelector = querySelector;
    }
    
    Login or Signup to reply.
  2. I would change the naming of maybeOverrideForTestStart to mockQuerySelector since its implementation changes as well.

    In order to correctly redefine/patch the modified querySelector implementation and also to exactly restore its default state one should choose an approach which makes use of the prototypal querySelector‘s property descriptor. The function would patch the modified version but also return a function which does restore the original setting, both via Object.defineProperty.

    // apply the monkey patch.
    const restoreDefault = mockQuerySelector(true, 'bar_123', 'foo');
    
    console.log(
      'after patching ... ', {
      restoreDefault,
      querySelector: Element.prototype.querySelector
    });
    
    // use the patched/modified version of `querySelector`
    console.log(
      "document.body.querySelector('script') ...",
      document.body.querySelector('script'),
    );
    
    // restore `querySelector` to its correct default state.
    restoreDefault();
    
    console.log(
      'after restoring ... ', {
      querySelector: Element.prototype.querySelector
    });
    
    // use the restored version of `querySelector`
    console.log(
      "document.body.querySelector('script') ...",
      document.body.querySelector('script'),
    );
    .as-console-wrapper { min-height: 100%!important; top: 0; }
    <script>
    // get the monkey patching right.
    
    function mockQuerySelector(isLocal, partialIDOfWidget, fullIDOfWidget) {
      if (!isLocal) return;
    
      const addonID = fullIDOfWidget.replace(partialIDOfWidget, "");
      const {
    
        // the original/native implementation.
        value: querySelector,
    
        // the original descriptor of the native implementation.
        ...queryConfig
    
      } = Object.getOwnPropertyDescriptor(Element.prototype, 'querySelector');
    
      // the modified `querySelector` implementation.
      function modifiedQuery(selector) {
    
        // apply the correct context to the original version.
        const element = querySelector.call(this, selector);
    
        if (element) {
          return element;
        }
        if (addonID) {
          return querySelector.call(this, selector + addonID);
        }
      };
    
      // - redefine the property via the default descriptor
      //   and the newly assigned modified function.
      Object
        .defineProperty(Element.prototype, 'querySelector', {
          value: modifiedQuery,
          ...queryConfig
        });
    
      function restoreDefault() {
        // - redefine/restore the property via the default descriptor
        //   and the locally stored original `querySelector` implementation.
        Object
          .defineProperty(Element.prototype, 'querySelector', {
            value: querySelector,
            ...queryConfig,
          });
      }
      return restoreDefault;
    }
    </script>
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search