summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/common/dispatcher/README.md
blob: cfaafb6e5d6496800afd40d04898bbbf946651ec (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
# `RemoteContext`: API for script execution in another context

`RemoteContext` in `/common/dispatcher/dispatcher.js` provides an interface to
execute JavaScript in another global object (page or worker, the "executor"),
based on:

- [WPT RFC 88: context IDs from uuid searchParams in URL](https://github.com/web-platform-tests/rfcs/pull/88),
- [WPT RFC 89: execute_script](https://github.com/web-platform-tests/rfcs/pull/89) and
- [WPT RFC 91: RemoteContext](https://github.com/web-platform-tests/rfcs/pull/91).

Tests can send arbitrary javascript to executors to evaluate in its global
object, like:

```
// injector.html
const argOnLocalContext = ...;

async function execute() {
  window.open('executor.html?uuid=' + uuid);
  const ctx = new RemoteContext(uuid);
  await ctx.execute_script(
      (arg) => functionOnRemoteContext(arg),
      [argOnLocalContext]);
};
```

and on executor:

```
// executor.html
function functionOnRemoteContext(arg) { ... }

const uuid = new URLSearchParams(window.location.search).get('uuid');
const executor = new Executor(uuid);
```

For concrete examples, see
[events.html](../../html/browsers/browsing-the-web/back-forward-cache/events.html)
and
[executor.html](../../html/browsers/browsing-the-web/back-forward-cache/resources/executor.html)
in back-forward cache tests.

Note that `executor*` files under `/common/dispatcher/` are NOT for
`RemoteContext.execute_script()`. Use `remote-executor.html` instead.

This is universal and avoids introducing many specific `XXX-helper.html`
resources.
Moreover, tests are easier to read, because the whole logic of the test can be
defined in a single file.

## `new RemoteContext(uuid)`

- `uuid` is a UUID string that identifies the remote context and should match
  with the `uuid` parameter of the URL of the remote context.
- Callers should create the remote context outside this constructor (e.g.
  `window.open('executor.html?uuid=' + uuid)`).

## `RemoteContext.execute_script(fn, args)`

- `fn` is a JavaScript function to execute on the remote context, which is
  converted to a string using `toString()` and sent to the remote context.
- `args` is null or an array of arguments to pass to the function on the
  remote context. Arguments are passed as JSON.
- If the return value of `fn` when executed in the remote context is a promise,
  the promise returned by `execute_script` resolves to the resolved value of
  that promise. Otherwise the `execute_script` promise resolves to the return
  value of `fn`.

Note that `fn` is evaluated on the remote context (`executor.html` in the
example above), while `args` are evaluated on the caller context
(`injector.html`) and then passed to the remote context.

## Return value of injected functions and `execute_script()`

If the return value of the injected function when executed in the remote
context is a promise, the promise returned by `execute_script` resolves to the
resolved value of that promise. Otherwise the `execute_script` promise resolves
to the return value of the function.

When the return value of an injected script is a Promise, it should be resolved
before any navigation starts on the remote context. For example, it shouldn't
be resolved after navigating out and navigating back to the page again.
It's fine to create a Promise to be resolved after navigations, if it's not the
return value of the injected function.

## Calling timing of `execute_script()`

When `RemoteContext.execute_script()` is called when the remote context is not
active (for example before it is created, before navigation to the page, or
during the page is in back-forward cache), the injected script is evaluated
after the remote context becomes active.

Multiple calls to `RemoteContext.execute_script()` will result in multiple scripts
being executed in remote context and ordering will be maintained.

## Errors from `execute_script()`

Errors from `execute_script()` will result in promise rejections, so it is
important to await the result.  This can be `await ctx.execute_script(...)` for
every call but if there are multiple scripts to executed, it may be preferable
to wait on them in parallel to avoid incurring full round-trip time for each,
e.g.

```js
await Promise.all(
  ctx1.execute_script(...),
  ctx1.execute_script(...),
  ctx2.execute_script(...),
  ctx2.execute_script(...),
  ...
)
```

## Evaluation timing of injected functions

The script injected by `RemoteContext.execute_script()` can be evaluated any
time during the remote context is active.
For example, even before DOMContentLoaded events or even during navigation.
It's the responsibility of test-specific code/helpers to ensure evaluation
timing constraints (which can be also test-specific), if any needed.

### Ensuring evaluation timing around page load

For example, to ensure that injected functions (`mainFunction` below) are
evaluated after the first `pageshow` event, we can use pure JavaScript code
like below:

```
// executor.html
window.pageShowPromise = new Promise(resolve =>
    window.addEventListener('pageshow', resolve, {once: true}));


// injector.html
const waitForPageShow = async () => {
  while (!window.pageShowPromise) {
    await new Promise(resolve => setTimeout(resolve, 100));
  }
  await window.pageShowPromise;
};

await ctx.execute(waitForPageShow);
await ctx.execute(mainFunction);
```

### Ensuring evaluation timing around navigation out/unloading

It can be important to ensure there are no injected functions nor code behind
`RemoteContext` (such as Fetch APIs accessing server-side stash) running after
navigation is initiated, for example in the case of back-forward cache testing.

To ensure this,

- Do not call the next `RemoteContext.execute()` for the remote context after
  triggering the navigation, until we are sure that the remote context is not
  active (e.g. after we confirm that the new page is loaded).
- Call `Executor.suspend(callback)` synchronously within the injected script.
  This suspends executor-related code, and calls `callback` when it is ready
  to start navigation.

The code on the injector side would be like:

```
// injector.html
await ctx.execute_script(() => {
  executor.suspend(() => {
    location.href = 'new-url.html';
  });
});
```

## Future Work: Possible integration with `test_driver`

Currently `RemoteContext` is implemented by JavaScript and WPT-server-side
stash, and not integrated with `test_driver` nor `testharness`.
There is a proposal of `test_driver`-integrated version (see the RFCs listed
above).

The API semantics and guidelines in this document are designed to be applicable
to both the current stash-based `RemoteContext` and `test_driver`-based
version, and thus the tests using `RemoteContext` will be migrated with minimum
modifications (mostly in `/common/dispatcher/dispatcher.js` and executors), for
example in a
[draft CL](https://chromium-review.googlesource.com/c/chromium/src/+/3082215/).


# `send()`/`receive()` Message passing APIs

`dispatcher.js` (and its server-side backend `dispatcher.py`) provides a
universal queue-based message passing API.
Each queue is identified by a UUID, and accessed via the following APIs:

-   `send(uuid, message)` pushes a string `message` to the queue `uuid`.
-   `receive(uuid)` pops the first item from the queue `uuid`.
-   `showRequestHeaders(origin, uuid)` and
    `cacheableShowRequestHeaders(origin, uuid)` return URLs, that push request
    headers to the queue `uuid` upon fetching.

It works cross-origin, and even access different browser context groups.

Messages are queued, this means one doesn't need to wait for the receiver to
listen, before sending the first message
(but still need to wait for the resolution of the promise returned by `send()`
to ensure the order between `send()`s).

## Executors

Similar to `RemoteContext.execute_script()`, `send()`/`receive()` can be used
for sending arbitrary javascript to be evaluated in another page or worker.

- `executor.html` (as a Document),
- `executor-worker.js` (as a Web Worker), and
- `executor-service-worker.js` (as a Service Worker)

are examples of executors.
Note that these executors are NOT compatible with
`RemoteContext.execute_script()`.

## Future Work

`send()`, `receive()` and the executors below are kept for COEP/COOP tests.

For remote script execution, new tests should use
`RemoteContext.execute_script()` instead.

For message passing,
[WPT RFC 90](https://github.com/web-platform-tests/rfcs/pull/90) is still under
discussion.