What is the process for monkeypatching the `querySelector` implementations for individual elements and documents, and then reverting them back to their original states?

Creating a local testing environment requires the temporary override of querySelector. Despite knowing that monkeypatching is generally frowned upon, in this specific instance, the code will only be utilized locally by developers. The following snippet has been crafted to achieve this (it overrides querySelector to retrieve all selectors containing another substring named 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;
  }

The maybeOverrideForTestStart function is called at the start of testing, while maybeOverrideForTestEnd is called at the end. However, the setup does not seem to be functioning properly and the issue remains unclear. Errors like

someElement.querySelector is not a function
or
"Uncaught TypeError: Illegal invocation"
are being encountered.

An additional concern is whether this override impacts document.querySelector and document.body.querySelector as well, or is limited to someElement.querySelector.

Your assistance in resolving this matter would be highly appreciated. Thank you.

Answer №1

I propose renaming maybeOverrideForTestStart to patchQuerySelectors as its functionality also changes.

To successfully redefine/patch the modified implementations of querySelector (this needs to be done for both Document.prototype and Element.prototype) and to accurately restore each default state, it is essential to choose an approach utilizing the property descriptor of each prototypal querySelector. The function patches each modified version while also providing a function that reverts each setting back to its original state using Object.defineProperty.

// Applying the monkey patch.
const restoreDefaults = patchQuerySelectors(true, 'bar_123', 'foo');

console.log(
  'after patching ... ', {
  restoreDefaults,
  elementQuery: Element.prototype.querySelector,
  documentQuery: Document.prototype.querySelector,
});

// Utilizing the patched/modified versions of `querySelector`.
console.log(
  "document.querySelector('body') ...",
  document.querySelector('body'),
);
console.log(
  "document.body.querySelector('script') ...",
  document.body.querySelector('script'),
);

// Restoring each specific `querySelector` to its correct default.
restoreDefaults();

console.log(
  'after restoring ... ', {
  elementQuery: Element.prototype.querySelector,
  documentQuery: Document.prototype.querySelector,
});

// Using the restored versions of `querySelector`
console.log(
  "document.querySelector('body') ...",
  document.querySelector('body'),
);
console.log(
  "document.body.querySelector('script') ...",
  document.body.querySelector('script'),
);
.as-console-wrapper { min-height: 100%!important; top: 0; }
<script>
// Ensuring proper implementation of the monkey patch.

function patchQuerySelectors(isLocal, partialIDOfWidget, fullIDOfWidget) {
  if (!isLocal) return;

  const addonID = fullIDOfWidget.replace(partialIDOfWidget, "");
  const {
    // Original/native implementation
    value: elementQuery,
    // Descriptor of the native implementation
    ...elementConfig
  } = Object.getOwnPropertyDescriptor(Element.prototype, 'querySelector');

  const {
    value: documentQuery,
    ...documentConfig
  } = Object.getOwnPropertyDescriptor(Document.prototype, 'querySelector');

  // Modified element specific `querySelector` implementation
  function modifiedElementQuery(selector) {

    // Apply the correct context to the original version
    const element = elementQuery.call(this, selector);

    if (element) {
      return element;
    }
    if (addonID) {
    // Apply the correct context to the original version
      return elementQuery.call(this, selector + addonID);
    }
  };
  // Modified document specific `querySelector` implementation
  function modifiedDocumentQuery(selector) {
    const element = documentQuery.call(this, selector);

    if (element) {
      return element;
    }
    if (addonID) {
      return documentQuery.call(this, selector + addonID);
    }
  };

  // Redefine the properties via the default descriptors and the newly assigned modified functions
  Object
    .defineProperty(Element.prototype, 'querySelector', {
      value: modifiedElementQuery,
      ...elementConfig
    });
  Object
    .defineProperty(Document.prototype, 'querySelector', {
      value: modifiedDocumentQuery,
      ...documentConfig
    });

  function restoreDefaults() {
    // Redefine/restore the properties via the default descriptors and the locally stored original `querySelector` implementations
    Object
      .defineProperty(Element.prototype, 'querySelector', {
        value: elementQuery,
        ...elementConfig,
      });
    Object
      .defineProperty(Document.prototype, 'querySelector', {
        value: documentQuery,
        ...documentConfig,
      });
  }
  return restoreDefaults;
}
</script>

Answer №2

If you're uncertain about the necessity to use .bind() on the function, one alternative approach could be to store a reference to the original function in a constant variable like this:

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;
}

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Tips on sending an array to material-ui dataSource props

Currently utilizing material-ui for a component. I am facing an issue with the autocomplete component in material-ui where I intend to display a list of icon names along with the icons. When only passing MenuItem to dataSource, it results in an empty input ...

Basic jQuery request for JSON data

In an effort to send user data to a PHP script and display the results in an element, I am utilizing JSON. The process works smoothly until reaching the response stage. Despite receiving the correct results when logging to the console, attempting to append ...

What is the best way to add form input to a variable without triggering a refresh?

Is there a way to dynamically add the First name input to the searchA.php in the second function located at the end of my code snippet? I'm looking for a solution that doesn't involve refreshing the page. <form action='' method=&apo ...

Invoking a class method through a dynamic function call with the use of setTimeout

In my JavaScript code, I have a structure similar to a class where I'm trying to call a function from the same level using a passed-in function name. Explaining this is a bit challenging, so let me illustrate with an example of what I'm aiming ...

JS Equal Heights is a JavaScript plugin designed to ensure

Good evening, A while back, I implemented a JavaScript code snippet to create equal height columns on my website. The initial script looked like this: <script type="text/javascript"> var maxHeight = 0; $(".level").each(function(){ maxHe ...

Error: Attempting to access a property of an undefined value, please verify the key name is

Attempting to incorporate dynamic elements into the assignment, I have written the following code: var contentList = Object.keys(content)[0]; $scope.model.currentLoadedContent[contentList] = content[Object.keys(content)[0]] When trying to execute the seco ...

Navigating to an ASPX page with details from a clicked event in FullCalendar - A step-by-step guide

I am currently working on a hybrid project involving FullCalendar in MVC and Webforms for other pages. When I click on an event, all the details are displayed as expected. Recently, I added a 'More Details' button to the 'eventClick' mo ...

Creating connections between variables and elements in nested ngRepeats in Angular

I've been facing a challenge with my app where I need to update the comments section whenever a comment is added, edited, or deleted without having to refresh the page. I am using ngResource to handle queries for both articles and comments (e.g. $scop ...

An efficient way to retrieve a value quickly?getValue:""

When working in the world of REACT, there are occasions where I need to verify the existence of a variable. If the value exists, I set it as is, but if not, I assign a fallback value. However, using the ( name = value ? value: "" ) expression ca ...

When the HTML and PHP code keeps running, the JavaScript text on the page updates itself

I was experimenting with threading in different languages like Java, PHP, and JavaScript. I am aware that JavaScript is single-threaded and PHP lacks robust threading capabilities. Below is the code snippet I have been working on: <body> <p id= ...

Select just one picture for emphasis

I have a collection of images on display and I am looking to make edits to specific image details. My goal is to be able to click on an image to highlight it, and then use the edit button to make changes. Currently, the functionality is in place, but I&a ...

Is there a way to make $ajax pause until data is received, then store it in a variable?

I am having trouble with disabling asynchronous ajax. Here is the code snippet that I have: function GetDataFromUninorte() { link="http://www.uninorte.edu.co/documents/71051/11558879/ExampleData.csv/0e3c22b1-0ec4-490d-86a2-d4bc4f512030" ...

Tips for maintaining the latest HTML, CSS, and JS files in Angular2/4 frontend hosted on IIS

When it comes to IIS, there are various settings that can affect the freshness of files. One setting involves Output Caching, where you can create cache rules such as enabling Kernel-mode caching and utilizing file change notifications. Another sett ...

HTML forms default values preset

I need help with pre-setting the values of a dropdown menu and text box in an HTML form for my iPhone app. When the user taps a button, it opens a webview and I want to preset the dropdown menu and text field. Can someone guide me on how to achieve this? ...

Remove console.log and alert statements from minified files using uglifyjs-folder

Currently, I am minifying multiple files in a directory using the uglifyjs-folder feature within my npm configuration in the package.json file as shown below: "uglifyjs": "uglifyjs-folder js -eyo build/js" The process is effectively minifying all the fil ...

Utilizing JavaScript Callbacks in HTML Image Tags

Currently I am working on a small application and looking to include a YouTube section. I have come across a method for obtaining the user's YouTube icon: This is included in the head section of my code: <script type="text/javascript" src="http:/ ...

Executing a function when clearing an autocomplete textfield in Material-UI

Currently, I am working with a material-ui autocomplete text field in order to filter a list. My goal is to have the list repopulate back to its original form when the user deletes the text and presses the enter key. After my research, it seems like the ...

transpile TypeScript code into ES6 rather than ES5

I am currently diving into typescript and I have observed that the code output always defaults to using var. Is there a way to make it output const and let in the .js file instead, or does typescript always require es5 output for some reason? Thanks. Her ...

Could we confirm if this straightforward string is considered valid JSON data?

There are numerous intricate questions on Stack Overflow about whether a complex structure is considered valid JSON. However, what about something much simpler? "12345" Would the provided code snippet be considered valid JSON? ...

Sequencing numerous promises (managing callbacks)

I am encountering some challenges with promises when it comes to chaining multiple ones. I'm having difficulty distinguishing how to effectively utilize promises and their differences with callbacks. I've noticed that sometimes callbacks are trig ...