"Simulating" JS Events
I’ve been working on my Google Workspace Zoom Default Chrome Extension to implement a requested feature where users can use custom zoom values in addition to the predefined zoom values that Google provides in their application’s zoom dropdown.
I thought to myself, “I already simulate events. This should be easy.” So, I started to try and figure it out. If you
want to skip ahead, feel free to go to the “Implementing with chrome.debugger
”
section.
But first, let’s have a look at how to “simulate events.”
Mouse Events
Simulating mouse events is pretty simple. Or, is it?
Turns out there’s many, many different ways to do it. Let’s start by figuring out what events are dispatched when a user clicks the button. Try it out in the example below.
As you can see, there are three events that trigger when you click once (we’ll only handle a single click).
These are (and executed in the following order):
mousedown
mouseup
click
Well well, there’s three events that trigger. Fair enough.
What is interesting though is that click
is at the end instead of the events instead of in the middle of the down and up
events. Something to think about 😉
Let’s try to replicate this now using just JS.
Using .click()
The first option you’d probably reach for (and myself), is using the click()
method that exists on an HTMLElement
.
Let’s give it a shot and see how it works.
After trying that, do you see the problem with trying to use click()
to simulate clicks with JS?
In case you didn’t, the problem is that when using the click()
method, the only event that is triggered when simulating
the click is the “click” event. We are missing out on the “mousedown” and “mouseup” events.
This may be fine for some things, but may not work for everything.
Dispatching Events
Let’s make this a bit more bulletproof. Instead of using the click()
method, we can actually go about dispatching events
on the element in question. To do this you use the EventTarget.dispatchEvent()
Something like below would dispatch a click event on the button.
const button = document.querySelector("button");
const event = new MouseEvent("click", {
view: window,
bubbles: true,
cancelable: true,
clientX: 0,
clientY: 0,
button: 0,
});
button.dispatchEvent(event);
Now, all we need to do is make into something that will execute all three events when a button is clicked. I’ve put it in the example below. Give it a shot.
🤯 BOOMMMMMM… Now we have finally simulated a “click” using JS. Finally 🔥
Gotchas (still…)
There’s still some potential gotchas when it comes to simulating these events.
This is due to the pesky isTrusted
property on the events. TL;DR - some browsers / apps will only respond to trusted
events. From MDN:
The isTrusted read-only property of the
Event
interface is a boolean value that is true when the event was generated by a user action, and false when the event was created or modified by a script or dispatched viaEventTarget.dispatchEvent()
.
This property cannot be faked, so when something won’t happen due to this, you’re pretty much out of luck (aside from what I’ll show you below, which is a hack and only available to Chrome Extensions).
While this isn’t a problem (for the most part) when simulating Mouse Events, it does become a problem when trying to simulate Keyboard Events, which I’ll demonstrate in the next section.
Keyboard Events
Cool cool. Keyboard Events are going to be just as easy. You would’ve thought…
Let’s see what triggers a Keyboard Event.
Depending on what key you hit the following events will fire(in order):
keydown
keypress
keyup
or, if hitting a key like “Shift”:
keydown
keyup
Now that we’ve gotten these events down, let’s try to simulate them. Good thing that we have KeyboardEvent
s that can
be dispatched just like MouseEvent
s. Let’s give it a shot.
Notice anything interesting?? In case you were too fixated on the “Console” window, have a look at the input. Unfortunately, while the events did fire there is nothing in the input box.
Remember how I talked about the isTrusted
property on the Event
(see above)? Well, this is very much what is happening
here. The browser is being smart enough to not let scripts insert values in to input boxes. Basically, it’s a security
concern and browsers are right in doing this.
So, where do we go from here? I still want to simulate these events. Maybe we can change the input’s value using JS. Let’s give it a shot.
Perfect. It looks like my script is typing in the box. Before you celebrate, what is happening in the “Console” window?
Turns out, nothing is happening. There are no events being triggered despite us having listeners on them. So, we are not “simulating” events.
Unfortunately, what we can do with MouseEvent
s is not possible with KeyboardEvents
due to browser security issues. We
can get pretty far in making it either:
- Dispatch the correct events
- OR… emulate typing
Trusted Keyboard Events with Chrome Debugger
So, if all that’s not possible, how am I going to implement the functionality in Google Workspace Zoom Default allowing my users to input custom zoom levels?
Let’s explore a “workaround” by using the Chrome Debugger available to Chrome Extensions.
🚨 This only works with Chrome Extensions that have access to the chrome.debugger
API 🚨
It turns out that Chrome Extensions can request access to the chrome.debugger
API.
Note: As I have to ask for an elevated set of permissions to make this work, I will be releasing a separate extension containing this functionality. I don’t want to force people into using something with which they’re not comfortable.
To start, this approach has the content script message the Background Service Worker (BGSW). The BGSW then attaches the debugger, generates the necessary commands, and detaches the debugger.
It’s relatively simple. It also shows a nice banner on your browser that shows what is happening (like it should).
The code looks something like below. I use Plasmo to implement this messaging, but it can be implemented via standard Message passing. Checkout the PR for more context on the changes.
// `req` is the request sent from the content script to the BGSW
const target = { tabId: req.tabId };
chrome.debugger.attach(target, "1.0");
chrome.debugger.sendCommand(target, "Input.insertText", {
text: req.body.zoomValue, // this value is sent from the content script
});
// The two events below have to be used together. I don't know why... but they do...
chrome.debugger.sendCommand(target, "Input.dispatchKeyEvent", {
type: "rawKeyDown",
code: "Enter",
key: "Enter",
macCharCode: 13,
nativeVirtualKeyCode: 13,
text: "\r",
unmodifiedText: "\r",
windowsVirtualKeyCode: 13,
});
chrome.debugger.sendCommand(target, "Input.dispatchKeyEvent", {
type: "char",
text: "\r",
});
chrome.debugger.detach(target);
I’ll hopefully be releasing the new Chrome Extension with this functionality soon 🤞
Until next time...