|
| 1 | +# `RemoteContext`: API for script execution in another context |
| 2 | + |
| 3 | +`RemoteContext` in `/common/dispatcher/dispatcher.js` provides an interface to |
| 4 | +execute JavaScript in another global object (page or worker, the "executor"), |
| 5 | +based on |
| 6 | + |
| 7 | +- [WPT RFC 88: context IDs from uuid searchParams in URL](https://github.com/web-platform-tests/rfcs/pull/88), |
| 8 | +- [WPT RFC 89: execute_script](https://github.com/web-platform-tests/rfcs/pull/89) and |
| 9 | +- [WPT RFC 91: RemoteContext](https://github.com/web-platform-tests/rfcs/pull/91). |
| 10 | + |
| 11 | +Tests can send arbitrary javascript to executors to evaluate in its global |
| 12 | +object, like: |
| 13 | + |
| 14 | +``` |
| 15 | +// injector.html |
| 16 | +const argOnLocalContext = ...; |
| 17 | +
|
| 18 | +async function execute() { |
| 19 | + window.open('executor.html?uuid=' + uuid); |
| 20 | + const ctx = new RemoteContext(uuid); |
| 21 | + await ctx.execute_script( |
| 22 | + (arg) => functionOnRemoteContext(arg), |
| 23 | + [argOnLocalContext]); |
| 24 | +}; |
| 25 | +``` |
| 26 | + |
| 27 | +and on executor: |
| 28 | + |
| 29 | +``` |
| 30 | +// executor.html |
| 31 | +function functionOnRemoteContext(arg) { ... } |
| 32 | +
|
| 33 | +const uuid = new URLSearchParams(window.location.search).get('uuid'); |
| 34 | +const executor = new Executor(uuid); |
| 35 | +``` |
| 36 | + |
| 37 | +For concrete examples, see |
| 38 | +[events.html](../../html/browsers/browsing-the-web/back-forward-cache/events.html) |
| 39 | +and |
| 40 | +[executor.html](../../html/browsers/browsing-the-web/back-forward-cache/resources/executor.html) |
| 41 | +in back-forward cache tests. |
| 42 | +Note that executor files under `/common/dispatcher/` are NOT for |
| 43 | +`RemoteContext.execute_script()`. |
| 44 | + |
| 45 | +This is universal and avoids introducing many specific `XXX-helper.html` |
| 46 | +resources. |
| 47 | +Moreover, tests are easier to read, because the whole logic of the test can be |
| 48 | +defined in a single file. |
| 49 | + |
| 50 | +## `new RemoteContext(uuid)` |
| 51 | + |
| 52 | +- `uuid` is a UUID string that identifies the remote context and should match |
| 53 | + with the `uuid` parameter of the URL of the remote context. |
| 54 | +- Callers should create the remote context outside this constructor (e.g. |
| 55 | + `window.open('executor.html?uuid=' + uuid)`). |
| 56 | + |
| 57 | +## `RemoteContext.execute_script(fn, args)` |
| 58 | + |
| 59 | +- `fn` is a JavaScript function to execute on the remote context, which is |
| 60 | + converted to a string using `toString()` and sent to the remote context. |
| 61 | +- `args` is null or an array of arguments to pass to the function on the |
| 62 | + remote context. Arguments are passed as JSON. |
| 63 | +- If the return value of `fn` when executed in the remote context is a promise, |
| 64 | + the promise returned by `execute_script` resolves to the resolved value of |
| 65 | + that promise. Otherwise the `execute_script` promise resolves to the return |
| 66 | + value of `fn`. |
| 67 | + |
| 68 | +Note that `fn` is evaluated on the remote context (`executor.html` in the |
| 69 | +example above), while `args` are evaluated on the caller context |
| 70 | +(`injector.html`) and then passed to the remote context. |
| 71 | + |
| 72 | +## Return value of injected functions and `execute_script()` |
| 73 | + |
| 74 | +If the return value of the injected function when executed in the remote |
| 75 | +context is a promise, the promise returned by `execute_script` resolves to the |
| 76 | +resolved value of that promise. Otherwise the `execute_script` promise resolves |
| 77 | +to the return value of the function. |
| 78 | + |
| 79 | +When the return value of an injected script is a Promise, it should be resolved |
| 80 | +before any navigation starts on the remote context. For example, it shouldn't |
| 81 | +be resolved after navigating out and navigating back to the page again. |
| 82 | +It's fine to create a Promise to be resolved after navigations, if it's not the |
| 83 | +return value of the injected function. |
| 84 | + |
| 85 | +## Calling timing of `execute_script()` |
| 86 | + |
| 87 | +When `RemoteContext.execute_script()` is called when the remote context is not |
| 88 | +active (for example before it is created, before navigation to the page, or |
| 89 | +during the page is in back-forward cache), the injected script is evaluated |
| 90 | +after the remote context becomes active. |
| 91 | + |
| 92 | +`RemoteContext.execute_script()` calls should be serialized by always waiting |
| 93 | +for the returned promise to be resolved. |
| 94 | +So it's a good practice to always write `await ctx.execute_script(...)`. |
| 95 | + |
| 96 | +## Evaluation timing of injected functions |
| 97 | + |
| 98 | +The script injected by `RemoteContext.execute_script()` can be evaluated any |
| 99 | +time during the remote context is active. |
| 100 | +For example, even before DOMContentLoaded events or even during navigation. |
| 101 | +It's the responsibility of test-specific code/helpers to ensure evaluation |
| 102 | +timing constraints (which can be also test-specific), if any needed. |
| 103 | + |
| 104 | +### Ensuring evaluation timing around page load |
| 105 | + |
| 106 | +For example, to ensure that injected functions (`mainFunction` below) are |
| 107 | +evaluated after the first `pageshow` event, we can use pure JavaScript code |
| 108 | +like below: |
| 109 | + |
| 110 | +``` |
| 111 | +// executor.html |
| 112 | +window.pageShowPromise = new Promise(resolve => |
| 113 | + window.addEventListener('pageshow', resolve, {once: true})); |
| 114 | +
|
| 115 | +
|
| 116 | +// injector.html |
| 117 | +const waitForPageShow = async () => { |
| 118 | + while (!window.pageShowPromise) { |
| 119 | + await new Promise(resolve => setTimeout(resolve, 100)); |
| 120 | + } |
| 121 | + await window.pageShowPromise; |
| 122 | +}; |
| 123 | +
|
| 124 | +await ctx.execute(waitForPageShow); |
| 125 | +await ctx.execute(mainFunction); |
| 126 | +``` |
| 127 | + |
| 128 | +### Ensuring evaluation timing around navigation out/unloading |
| 129 | + |
| 130 | +It can be important to ensure there are no injected functions nor code behind |
| 131 | +`RemoteContext` (such as Fetch APIs accessing server-side stash) running after |
| 132 | +navigation is initiated, for example in the case of back-forward cache testing. |
| 133 | + |
| 134 | +To ensure this, |
| 135 | + |
| 136 | +- Do not call the next `RemoteContext.execute()` for the remote context after |
| 137 | + triggering the navigation, until we are sure that the remote context is not |
| 138 | + active (e.g. after we confirm that the new page is loaded). |
| 139 | +- Call `Executor.suspend(callback)` synchronously within the injected script. |
| 140 | + This suspends executor-related code, and calls `callback` when it is ready |
| 141 | + to start navigation. |
| 142 | + |
| 143 | +The code on the injector side would be like: |
| 144 | + |
| 145 | +``` |
| 146 | +// injector.html |
| 147 | +await ctx.execute_script(() => { |
| 148 | + executor.suspend(() => { |
| 149 | + location.href = 'new-url.html'; |
| 150 | + }); |
| 151 | +}); |
| 152 | +``` |
| 153 | + |
| 154 | +## Future Work: Possible integration with `test_driver` |
| 155 | + |
| 156 | +Currently `RemoteContext` is implemented by JavaScript and WPT-server-side |
| 157 | +stash, and not integrated with `test_driver` nor `testharness`. |
| 158 | +There is a proposal of `test_driver`-integrated version (see the RFCs listed |
| 159 | +above). |
| 160 | + |
| 161 | +The API semantics and guidelines in this document are designed to be applicable |
| 162 | +to both the current stash-based `RemoteContext` and `test_driver`-based |
| 163 | +version, and thus the tests using `RemoteContext` will be migrated with minimum |
| 164 | +modifications (mostly in `/common/dispatcher/dispatcher.js` and executors), for |
| 165 | +example in a |
| 166 | +[draft CL](https://chromium-review.googlesource.com/c/chromium/src/+/3082215/). |
| 167 | + |
| 168 | + |
| 169 | +# `send()`/`receive()` Message passing APIs |
| 170 | + |
| 171 | +`dispatcher.js` (and its server-side backend `dispatcher.py`) provides a |
| 172 | +universal queue-based message passing API. |
| 173 | +Each queue is identified by a UUID, and accessed via the following APIs: |
| 174 | + |
| 175 | +- `send(uuid, message)` pushes a string `message` to the queue `uuid`. |
| 176 | +- `receive(uuid)` pops the first item from the queue `uuid`. |
| 177 | +- `showRequestHeaders(origin, uuid)` and |
| 178 | + `cacheableShowRequestHeaders(origin, uuid)` return URLs, that push request |
| 179 | + headers to the queue `uuid` upon fetching. |
| 180 | + |
| 181 | +It works cross-origin, and even access different browser context groups. |
| 182 | + |
| 183 | +Messages are queued, this means one doesn't need to wait for the receiver to |
| 184 | +listen, before sending the first message |
| 185 | +(but still need to wait for the resolution of the promise returned by `send()` |
| 186 | +to ensure the order between `send()`s). |
| 187 | + |
| 188 | +## Executors |
| 189 | + |
| 190 | +Similar to `RemoteContext.execute_script()`, `send()`/`receive()` can be used |
| 191 | +for sending arbitrary javascript to be evaluated in another page or worker. |
| 192 | + |
| 193 | +- `executor.html` (as a Document), |
| 194 | +- `executor-worker.js` (as a Web Worker), and |
| 195 | +- `executor-service-worker.js` (as a Service Worker) |
| 196 | + |
| 197 | +are examples of executors. |
| 198 | +Note that these executors are NOT compatible with |
| 199 | +`RemoteContext.execute_script()`. |
| 200 | + |
| 201 | +## Future Work |
| 202 | + |
| 203 | +`send()`, `receive()` and the executors below are kept for COEP/COOP tests. |
| 204 | + |
| 205 | +For remote script execution, new tests should use |
| 206 | +`RemoteContext.execute_script()` instead. |
| 207 | + |
| 208 | +For message passing, |
| 209 | +[WPT RFC 90](https://github.com/web-platform-tests/rfcs/pull/90) is still under |
| 210 | +discussion. |
0 commit comments