| Yuzhu Shen | e70d197 | 2017-06-02 16:35:15 | [diff] [blame] | 1 | # Mojo JavaScript Bindings API |
| Mathias Bynens | 6c5c87d | 2024-02-21 12:17:16 | [diff] [blame] | 2 | |
| Ken Rockot | 929282c | 2018-05-02 17:07:29 | [diff] [blame] | 3 | This document is a subset of the [Mojo documentation](/mojo/README.md). |
| rockot | f59d2d6 | 2017-04-01 02:49:08 | [diff] [blame] | 4 | |
| Oksana Zhuravlova | 9f3b8ef | 2019-08-26 20:27:40 | [diff] [blame] | 5 | *** note |
| Alison Gale | 81f4f2c7 | 2024-04-22 19:33:31 | [diff] [blame] | 6 | TODO(crbug.com/40605290): this document mentions deprecated JavaScript bindings. |
| Oksana Zhuravlova | 9f3b8ef | 2019-08-26 20:27:40 | [diff] [blame] | 7 | |
| 8 | We need to update it to describe the new bindings (bindings_lite). |
| 9 | *** |
| 10 | |
| Yuzhu Shen | e70d197 | 2017-06-02 16:35:15 | [diff] [blame] | 11 | [TOC] |
| 12 | |
| 13 | ## Getting Started |
| Mathias Bynens | 6c5c87d | 2024-02-21 12:17:16 | [diff] [blame] | 14 | |
| Yuzhu Shen | e70d197 | 2017-06-02 16:35:15 | [diff] [blame] | 15 | The bindings API is defined in the `mojo` namespace and implemented in |
| 16 | `mojo_bindings.js`, which could be generated by the GN target |
| Yuzhu Shen | 4ccd269 | 2017-12-11 19:14:25 | [diff] [blame] | 17 | `//mojo/public/js:bindings`. |
| Yuzhu Shen | e70d197 | 2017-06-02 16:35:15 | [diff] [blame] | 18 | |
| 19 | When a Mojom IDL file is processed by the bindings generator, JavaScript code is |
| 20 | emitted in a `.js` file with the name based on the input `.mojom` file. Suppose |
| 21 | we create the following Mojom file at |
| 22 | `//services/echo/public/interfaces/echo.mojom`: |
| 23 | |
| 24 | ``` |
| 25 | module test.echo.mojom; |
| 26 | |
| 27 | interface Echo { |
| 28 | EchoInteger(int32 value) => (int32 result); |
| 29 | }; |
| 30 | ``` |
| 31 | |
| 32 | And a GN target to generate the bindings in |
| 33 | `//services/echo/public/interfaces/BUILD.gn`: |
| 34 | |
| 35 | ``` |
| 36 | import("//mojo/public/tools/bindings/mojom.gni") |
| 37 | |
| 38 | mojom("interfaces") { |
| 39 | sources = [ |
| 40 | "echo.mojom", |
| 41 | ] |
| 42 | } |
| 43 | ``` |
| 44 | |
| Gary Klassen | dc9b625 | 2017-08-18 19:57:46 | [diff] [blame] | 45 | Bindings are generated by building one of these implicitly generated targets |
| 46 | (where "foo" is the target name): |
| Mathias Bynens | 6c5c87d | 2024-02-21 12:17:16 | [diff] [blame] | 47 | |
| 48 | - `foo_js` JavaScript bindings; used as compile-time dependency. |
| 49 | - `foo_js_data_deps` JavaScript bindings; used as run-time dependency. |
| Gary Klassen | dc9b625 | 2017-08-18 19:57:46 | [diff] [blame] | 50 | |
| Yuzhu Shen | e70d197 | 2017-06-02 16:35:15 | [diff] [blame] | 51 | If we then build this target: |
| Mathias Bynens | 6c5c87d | 2024-02-21 12:17:16 | [diff] [blame] | 52 | |
| Yuzhu Shen | e70d197 | 2017-06-02 16:35:15 | [diff] [blame] | 53 | ``` |
| Gary Klassen | dc9b625 | 2017-08-18 19:57:46 | [diff] [blame] | 54 | ninja -C out/r services/echo/public/interfaces:interfaces_js |
| Yuzhu Shen | e70d197 | 2017-06-02 16:35:15 | [diff] [blame] | 55 | ``` |
| 56 | |
| 57 | This will produce several generated source files. The one relevant to JavaScript |
| 58 | bindings is: |
| Mathias Bynens | 6c5c87d | 2024-02-21 12:17:16 | [diff] [blame] | 59 | |
| Yuzhu Shen | e70d197 | 2017-06-02 16:35:15 | [diff] [blame] | 60 | ``` |
| 61 | out/gen/services/echo/public/interfaces/echo.mojom.js |
| 62 | ``` |
| 63 | |
| 64 | In order to use the definitions in `echo.mojom`, you will need to include two |
| 65 | files in your html page using `<script>` tags: |
| Yuzhu Shen | e70d197 | 2017-06-02 16:35:15 | [diff] [blame] | 66 | |
| Mathias Bynens | 6c5c87d | 2024-02-21 12:17:16 | [diff] [blame] | 67 | - `mojo_bindings.js` **Note: This file must be included before any `.mojom.js` |
| 68 | files.** |
| 69 | - `echo.mojom.js` |
| 70 | |
| 71 | ```html |
| 72 | <!doctype html> |
| Yuzhu Shen | e70d197 | 2017-06-02 16:35:15 | [diff] [blame] | 73 | <script src="URL/to/mojo_bindings.js"></script> |
| 74 | <script src="URL/to/echo.mojom.js"></script> |
| 75 | <script> |
| Mathias Bynens | 6c5c87d | 2024-02-21 12:17:16 | [diff] [blame] | 76 | const echoPtr = new test.echo.mojom.EchoPtr(); |
| 77 | const echoRequest = mojo.makeRequest(echoPtr); |
| 78 | // ... |
| Yuzhu Shen | e70d197 | 2017-06-02 16:35:15 | [diff] [blame] | 79 | </script> |
| 80 | ``` |
| 81 | |
| 82 | ## Interfaces |
| Mathias Bynens | 6c5c87d | 2024-02-21 12:17:16 | [diff] [blame] | 83 | |
| Yuzhu Shen | e70d197 | 2017-06-02 16:35:15 | [diff] [blame] | 84 | Similar to the C++ bindings API, we have: |
| Mathias Bynens | 6c5c87d | 2024-02-21 12:17:16 | [diff] [blame] | 85 | |
| 86 | - `mojo.InterfacePtrInfo` and `mojo.InterfaceRequest` encapsulate two ends of a |
| Yuzhu Shen | e70d197 | 2017-06-02 16:35:15 | [diff] [blame] | 87 | message pipe. They represent the client end and service end of an interface |
| 88 | connection, respectively. |
| Mathias Bynens | 6c5c87d | 2024-02-21 12:17:16 | [diff] [blame] | 89 | - For each Mojom interface `Foo`, there is a generated `FooPtr` class. It owns |
| Yuzhu Shen | e70d197 | 2017-06-02 16:35:15 | [diff] [blame] | 90 | an `InterfacePtrInfo`; provides methods to send interface calls using the |
| 91 | message pipe handle from the `InterfacePtrInfo`. |
| Mathias Bynens | 6c5c87d | 2024-02-21 12:17:16 | [diff] [blame] | 92 | - `mojo.Binding` owns an `InterfaceRequest`. It listens on the message pipe |
| Yuzhu Shen | e70d197 | 2017-06-02 16:35:15 | [diff] [blame] | 93 | handle and dispatches incoming messages to a user-defined interface |
| 94 | implementation. |
| 95 | |
| 96 | Let's consider the `echo.mojom` example above. The following shows how to create |
| 97 | an `Echo` interface connection and use it to make a call. |
| 98 | |
| Mathias Bynens | 6c5c87d | 2024-02-21 12:17:16 | [diff] [blame] | 99 | ```html |
| 100 | <!doctype html> |
| Yuzhu Shen | e70d197 | 2017-06-02 16:35:15 | [diff] [blame] | 101 | <script src="URL/to/mojo_bindings.js"></script> |
| 102 | <script src="URL/to/echo.mojom.js"></script> |
| 103 | <script> |
| Mathias Bynens | 6c5c87d | 2024-02-21 12:17:16 | [diff] [blame] | 104 | function EchoImpl() {} |
| 105 | EchoImpl.prototype.echoInteger = function (value) { |
| 106 | return Promise.resolve({ result: value }); |
| 107 | }; |
| Yuzhu Shen | e70d197 | 2017-06-02 16:35:15 | [diff] [blame] | 108 | |
| Mathias Bynens | 6c5c87d | 2024-02-21 12:17:16 | [diff] [blame] | 109 | const echoServicePtr = new test.echo.mojom.EchoPtr(); |
| 110 | const echoServiceRequest = mojo.makeRequest(echoServicePtr); |
| 111 | const echoServiceBinding = new mojo.Binding( |
| 112 | test.echo.mojom.Echo, |
| 113 | new EchoImpl(), |
| 114 | echoServiceRequest, |
| 115 | ); |
| 116 | echoServicePtr.echoInteger({ value: 123 }).then((response) => { |
| 117 | console.log(`The result is ${response.value}`); |
| 118 | }); |
| Yuzhu Shen | e70d197 | 2017-06-02 16:35:15 | [diff] [blame] | 119 | </script> |
| 120 | ``` |
| 121 | |
| 122 | ### Interface Pointers and Requests |
| Mathias Bynens | 6c5c87d | 2024-02-21 12:17:16 | [diff] [blame] | 123 | |
| Yuzhu Shen | e70d197 | 2017-06-02 16:35:15 | [diff] [blame] | 124 | In the example above, `test.echo.mojom.EchoPtr` is an interface pointer class. |
| 125 | `EchoPtr` represents the client end of an interface connection. For method |
| 126 | `EchoInteger` in the `Echo` Mojom interface, there is a corresponding |
| 127 | `echoInteger` method defined in `EchoPtr`. (Please note that the format of the |
| 128 | generated method name is `camelCaseWithLowerInitial`.) |
| 129 | |
| 130 | There are some control methods shared by all interface pointer classes. For |
| 131 | example, binding/extracting `InterfacePtrInfo`, setting connection error |
| 132 | handler, querying version information, etc. In order to avoid name collision, |
| 133 | they are defined in `mojo.InterfacePtrController` and exposed as the `ptr` field |
| 134 | of every interface pointer class. |
| 135 | |
| 136 | In the example above, `echoServiceRequest` is an `InterfaceRequest` instance. It |
| 137 | represents the service end of an interface connection. |
| 138 | |
| 139 | `mojo.makeRequest` creates a message pipe; populates the output argument (which |
| 140 | could be an `InterfacePtrInfo` or an interface pointer) with one end of the |
| 141 | pipe; returns the other end wrapped in an `InterfaceRequest` instance. |
| 142 | |
| 143 | ### Binding an InterfaceRequest |
| Mathias Bynens | 6c5c87d | 2024-02-21 12:17:16 | [diff] [blame] | 144 | |
| Yuzhu Shen | e70d197 | 2017-06-02 16:35:15 | [diff] [blame] | 145 | A `mojo.Binding` bridges an implementation of an interface and a message pipe |
| 146 | endpoint, dispatching incoming messages to the implementation. |
| 147 | |
| 148 | In the example above, `echoServiceBinding` listens for incoming `EchoInteger` |
| 149 | method calls on the messsage pipe, and dispatches those calls to the `EchoImpl` |
| 150 | instance. |
| 151 | |
| 152 | ### Receiving Responses |
| Mathias Bynens | 6c5c87d | 2024-02-21 12:17:16 | [diff] [blame] | 153 | |
| Yuzhu Shen | e70d197 | 2017-06-02 16:35:15 | [diff] [blame] | 154 | Some Mojom interface methods expect a response, such as `EchoInteger`. The |
| 155 | corresponding JavaScript method returns a Promise. This Promise is resolved when |
| 156 | the service side sends back a response. It is rejected if the interface is |
| 157 | disconnected. |
| 158 | |
| 159 | ### Connection Errors |
| Mathias Bynens | 6c5c87d | 2024-02-21 12:17:16 | [diff] [blame] | 160 | |
| Yuzhu Shen | 92e791aa | 2017-06-20 20:39:31 | [diff] [blame] | 161 | If a pipe is disconnected, both endpoints will be able to observe the connection |
| 162 | error (unless the disconnection is caused by closing/destroying an endpoint, in |
| 163 | which case that endpoint won't get such a notification). If there are remaining |
| 164 | incoming messages for an endpoint on disconnection, the connection error won't |
| 165 | be triggered until the messages are drained. |
| 166 | |
| 167 | Pipe disconnecition may be caused by: |
| Mathias Bynens | 6c5c87d | 2024-02-21 12:17:16 | [diff] [blame] | 168 | |
| 169 | - Mojo system-level causes: process terminated, resource exhausted, etc. |
| 170 | - The bindings close the pipe due to a validation error when processing a |
| Yuzhu Shen | 92e791aa | 2017-06-20 20:39:31 | [diff] [blame] | 171 | received message. |
| Mathias Bynens | 6c5c87d | 2024-02-21 12:17:16 | [diff] [blame] | 172 | - The peer endpoint is closed. For example, the remote side is a bound interface |
| Yuzhu Shen | 92e791aa | 2017-06-20 20:39:31 | [diff] [blame] | 173 | pointer and it is destroyed. |
| Yuzhu Shen | e70d197 | 2017-06-02 16:35:15 | [diff] [blame] | 174 | |
| Mathias Bynens | 6c5c87d | 2024-02-21 12:17:16 | [diff] [blame] | 175 | Regardless of the underlying cause, when a connection error is encountered on a |
| 176 | binding endpoint, that endpoint's **connection error handler** (if set) is |
| 177 | invoked. This handler may only be invoked _once_ as long as the endpoint is |
| Yuzhu Shen | e70d197 | 2017-06-02 16:35:15 | [diff] [blame] | 178 | bound to the same pipe. Typically clients and implementations use this handler |
| 179 | to do some kind of cleanup or recovery. |
| 180 | |
| Mathias Bynens | 6c5c87d | 2024-02-21 12:17:16 | [diff] [blame] | 181 | ```js |
| Yuzhu Shen | e70d197 | 2017-06-02 16:35:15 | [diff] [blame] | 182 | // Assume echoServicePtr is already bound. |
| Mathias Bynens | 6c5c87d | 2024-02-21 12:17:16 | [diff] [blame] | 183 | echoServicePtr.ptr.setConnectionErrorHandler(function () { |
| Yuzhu Shen | e70d197 | 2017-06-02 16:35:15 | [diff] [blame] | 184 | DoImportantCleanUp(); |
| 185 | }); |
| 186 | |
| 187 | // Assume echoServiceBinding is already bound: |
| Mathias Bynens | 6c5c87d | 2024-02-21 12:17:16 | [diff] [blame] | 188 | echoServiceBinding.setConnectionErrorHandler(function () { |
| Yuzhu Shen | e70d197 | 2017-06-02 16:35:15 | [diff] [blame] | 189 | DoImportantCleanUpToo(); |
| 190 | }); |
| 191 | ``` |
| 192 | |
| 193 | **Note:** Closing one end of a pipe will eventually trigger a connection error |
| Mathias Bynens | 6c5c87d | 2024-02-21 12:17:16 | [diff] [blame] | 194 | on the other end. However it's ordered with respect to any other event (_e.g._ |
| Yuzhu Shen | e70d197 | 2017-06-02 16:35:15 | [diff] [blame] | 195 | writing a message) on the pipe. Therefore, it is safe to make an `echoInteger` |
| 196 | call on `echoServicePtr` and reset it immediately (which results in |
| 197 | disconnection), `echoServiceBinding` will receive the `echoInteger` call before |
| 198 | it observes the connection error. |
| 199 | |
| 200 | ## Associated Interfaces |
| Mathias Bynens | 6c5c87d | 2024-02-21 12:17:16 | [diff] [blame] | 201 | |
| Yuzhu Shen | e70d197 | 2017-06-02 16:35:15 | [diff] [blame] | 202 | An associated interface connection doesn't have its own underlying message pipe. |
| 203 | It is associated with an existing message pipe (i.e., interface connection). |
| 204 | |
| 205 | Similar to the non-associated interface case, we have: |
| Mathias Bynens | 6c5c87d | 2024-02-21 12:17:16 | [diff] [blame] | 206 | |
| 207 | - `mojo.AssociatedInterfacePtrInfo` and `mojo.AssociatedInterfaceRequest` |
| 208 | encapsulate a _route ID_, representing a logical connection over a message |
| Yuzhu Shen | e70d197 | 2017-06-02 16:35:15 | [diff] [blame] | 209 | pipe. |
| Mathias Bynens | 6c5c87d | 2024-02-21 12:17:16 | [diff] [blame] | 210 | - For each Mojom interface `Foo`, there is a generated `FooAssociatedPtr` class. |
| Yuzhu Shen | e70d197 | 2017-06-02 16:35:15 | [diff] [blame] | 211 | It owns an `AssociatedInterfacePtrInfo`. It is the client side of an |
| 212 | interface. |
| Mathias Bynens | 6c5c87d | 2024-02-21 12:17:16 | [diff] [blame] | 213 | - `mojo.AssociatedBinding` owns an `AssociatedInterfaceRequest`. It listens on |
| Yuzhu Shen | e70d197 | 2017-06-02 16:35:15 | [diff] [blame] | 214 | the connection and dispatches incoming messages to a user-defined interface |
| 215 | implementation. |
| 216 | |
| Mathias Bynens | 6c5c87d | 2024-02-21 12:17:16 | [diff] [blame] | 217 | See |
| 218 | [this document](https://www.chromium.org/developers/design-documents/mojo/associated-interfaces) |
| Yuzhu Shen | e70d197 | 2017-06-02 16:35:15 | [diff] [blame] | 219 | for more details. |
| 220 | |
| 221 | ## Automatic and Manual Dependency Loading |
| Mathias Bynens | 6c5c87d | 2024-02-21 12:17:16 | [diff] [blame] | 222 | |
| Yuzhu Shen | e70d197 | 2017-06-02 16:35:15 | [diff] [blame] | 223 | By default, generated `.mojom.js` files automatically load Mojom dependencies. |
| 224 | For example, if `foo.mojom` imports `bar.mojom`, loading `foo.mojom.js` will |
| 225 | insert a `<script>` tag to load `bar.mojom.js`, if it hasn't been loaded. |
| 226 | |
| 227 | The URL of `bar.mojom.js` is determined by: |
| Yuzhu Shen | e70d197 | 2017-06-02 16:35:15 | [diff] [blame] | 228 | |
| Mathias Bynens | 6c5c87d | 2024-02-21 12:17:16 | [diff] [blame] | 229 | - the path of `bar.mojom` relative to the position of `foo.mojom` at build time; |
| 230 | - the URL of `foo.mojom.js`. |
| 231 | |
| 232 | For example, if at build time the two Mojom files are located at: |
| 233 | |
| Yuzhu Shen | e70d197 | 2017-06-02 16:35:15 | [diff] [blame] | 234 | ``` |
| 235 | a/b/c/foo.mojom |
| 236 | a/b/d/bar.mojom |
| 237 | ``` |
| 238 | |
| 239 | The URL of `foo.mojom.js` is: |
| Mathias Bynens | 6c5c87d | 2024-02-21 12:17:16 | [diff] [blame] | 240 | |
| Yuzhu Shen | e70d197 | 2017-06-02 16:35:15 | [diff] [blame] | 241 | ``` |
| 242 | http://example.org/scripts/b/c/foo.mojom.js |
| 243 | ``` |
| 244 | |
| 245 | Then the URL of `bar.mojom.js` is supposed to be: |
| Mathias Bynens | 6c5c87d | 2024-02-21 12:17:16 | [diff] [blame] | 246 | |
| Yuzhu Shen | e70d197 | 2017-06-02 16:35:15 | [diff] [blame] | 247 | ``` |
| 248 | http://example.org/scripts/b/d/bar.mojom.js |
| 249 | ``` |
| 250 | |
| 251 | If you would like `bar.mojom.js` to live at a different location, you need to |
| 252 | set `mojo.config.autoLoadMojomDeps` to `false` before loading `foo.mojom.js`, |
| 253 | and manually load `bar.mojom.js` yourself. Similarly, you need to turn off this |
| 254 | option if you merge `bar.mojom.js` and `foo.mojom.js` into a single file. |
| 255 | |
| Mathias Bynens | 6c5c87d | 2024-02-21 12:17:16 | [diff] [blame] | 256 | ```html |
| Yuzhu Shen | e70d197 | 2017-06-02 16:35:15 | [diff] [blame] | 257 | <!-- Automatic dependency loading --> |
| 258 | <script src="http://example.org/scripts/mojo_bindings.js"></script> |
| 259 | <script src="http://example.org/scripts/b/c/foo.mojom.js"></script> |
| 260 | |
| Yuzhu Shen | e70d197 | 2017-06-02 16:35:15 | [diff] [blame] | 261 | <!-- Manual dependency loading --> |
| 262 | <script src="http://example.org/scripts/mojo_bindings.js"></script> |
| 263 | <script> |
| 264 | mojo.config.autoLoadMojomDeps = false; |
| 265 | </script> |
| 266 | <script src="http://example.org/scripts/b/d/bar.mojom.js"></script> |
| 267 | <script src="http://example.org/scripts/b/c/foo.mojom.js"></script> |
| 268 | ``` |
| 269 | |
| Mathias Bynens | 6c5c87d | 2024-02-21 12:17:16 | [diff] [blame] | 270 | ### Performance Tip: Avoid Loading the Same `.mojom.js` File Multiple Times |
| 271 | |
| Yuzhu Shen | e70d197 | 2017-06-02 16:35:15 | [diff] [blame] | 272 | If `mojo.config.autoLoadMojomDeps` is set to `true` (which is the default |
| 273 | value), you might accidentally load the same `.mojom.js` file multiple times if |
| 274 | you are not careful. Although it doesn't cause fatal errors, it hurts |
| 275 | performance and therefore should be avoided. |
| 276 | |
| Mathias Bynens | 6c5c87d | 2024-02-21 12:17:16 | [diff] [blame] | 277 | ```html |
| Yuzhu Shen | e70d197 | 2017-06-02 16:35:15 | [diff] [blame] | 278 | <!-- Assume that mojo.config.autoLoadMojomDeps is set to true: --> |
| 279 | |
| 280 | <!-- No duplicate loading; recommended. --> |
| 281 | <script src="http://example.org/scripts/b/c/foo.mojom.js"></script> |
| 282 | |
| 283 | <!-- No duplicate loading, although unnecessary. --> |
| 284 | <script src="http://example.org/scripts/b/d/bar.mojom.js"></script> |
| 285 | <script src="http://example.org/scripts/b/c/foo.mojom.js"></script> |
| 286 | |
| 287 | <!-- Load bar.mojom.js twice; should be avoided. --> |
| 288 | <!-- when foo.mojom.js is loaded, it sees that bar.mojom.js is not yet loaded, |
| 289 | so it inserts another <script> tag for bar.mojom.js. --> |
| 290 | <script src="http://example.org/scripts/b/c/foo.mojom.js"></script> |
| 291 | <script src="http://example.org/scripts/b/d/bar.mojom.js"></script> |
| 292 | ``` |
| 293 | |
| 294 | If a `.mojom.js` file is loaded for a second time, a warnings will be showed |
| 295 | using `console.warn()` to bring it to developers' attention. |
| 296 | |
| 297 | ## Name Formatting |
| Mathias Bynens | 6c5c87d | 2024-02-21 12:17:16 | [diff] [blame] | 298 | |
| Yuzhu Shen | e70d197 | 2017-06-02 16:35:15 | [diff] [blame] | 299 | As a general rule, Mojom definitions follow the C++ formatting style. To make |
| 300 | the generated JavaScript bindings conforms to our JavaScript style guide, the |
| 301 | code generator does the following conversions: |
| 302 | |
| Mathias Bynens | 6c5c87d | 2024-02-21 12:17:16 | [diff] [blame] | 303 | | In Mojom | In generated `.mojom.js` | |
| 304 | | ---------------------- | ------------------------ | |
| 305 | | `MethodLikeThis` | `methodLikeThis` | |
| 306 | | `parameter_like_this` | `parameterLikeThis` | |
| 307 | | `field_like_this` | `fieldLikeThis` | |
| 308 | | `name_space.like_this` | `nameSpace.likeThis` | |
| 309 | |
| 310 | ## Downloads |
| 311 | |
| 312 | As of Chrome 123.0.6309.0, prebuilt MojoJS archives are available for download |
| 313 | via |
| 314 | [Chrome for Testing (CfT) infrastructure](https://developer.chrome.com/blog/chrome-for-testing). |
| 315 | For every user-facing Chrome release, a correspondingly-versioned ZIP file |
| 316 | containing the relevant MojoJS bindings can be downloaded at the following URL: |
| 317 | |
| 318 | ``` |
| 319 | https://storage.googleapis.com/chrome-for-testing-public/123.0.6309.0/mojojs.zip |
| 320 | ``` |
| 321 | |
| 322 | Replace `123.0.6309.0` with the exact Chrome version you need. Any version in |
| 323 | [CfT’s `known-good-versions.json`](https://googlechromelabs.github.io/chrome-for-testing/known-good-versions.json) |
| 324 | greater than or equal to 123.0.6309.0 is guaranteed to have a corresponding |
| 325 | `mojojs.zip` download available. |