@@ -66,8 +66,6 @@ browser-specific techniques.
66
66
via the main test window, make it hard to build an ergonomic
67
67
cross-context messaging API.
68
68
69
- ### Proposal
70
-
71
69
All the above examples of prior art have some attractive features, and
72
70
it's possible to combine them in a way that should provide an
73
71
ergonomic API for web-platform-tests
@@ -84,6 +82,8 @@ ergonomic API for web-platform-tests
84
82
messages, and the relatively low level API based on passing strings
85
83
around, seem like areas for improvement.
86
84
85
+ ### Proposal
86
+
87
87
The following subsections will set out a proposal for an API that
88
88
combines some of these strengths.
89
89
@@ -150,7 +150,7 @@ functions shutdown when the socket is closed.
150
150
#### Remote Object Serialization
151
151
152
152
In order to allow passing complex JavaScript objects between contexts,
153
- a serialization format based on the [ WebDriver
153
+ a serialization format loosely based on the [ WebDriver
154
154
BiDi] ( https://w3c.github.io/webdriver-bidi/#type-common-RemoteValue )
155
155
proposal is used. An object is represented using a JSON object as
156
156
follows:
@@ -159,16 +159,16 @@ follows:
159
159
type: Name of the object type,
160
160
value: A representation of the object in a JSON - compatible form
161
161
where possible,
162
- objectId: A unique id assigned to the object (not for primitives)
162
+ objectId: A unique id assigned to the object in case of circular references.
163
163
}
164
164
```
165
165
166
- So, for example, the array ` [1, "foo", {"bar": null}] ` is represented as:
166
+ So, for example, an array ` a ` with content ` [1, "foo", {"bar": null}, a ] ` is represented as:
167
167
168
168
``` js
169
169
{
170
170
" type" : " array" ,
171
- " objectId" : < uuid > ,
171
+ " objectId" : 0 ,
172
172
" value" : [
173
173
{
174
174
type: " number" ,
@@ -178,32 +178,46 @@ So, for example, the array `[1, "foo", {"bar": null}]` is represented as:
178
178
" type" : " string" ,
179
179
" value" : " foo"
180
180
},
181
+ {
181
182
" type" : " object" ,
182
- " objectId" : < uuid> ,
183
183
" value" : {
184
184
" bar" : {
185
185
" type" : null
186
186
}
187
187
}
188
+ },
189
+ {
190
+ " type" : " array" ,
191
+ " objectId" : 0
192
+ }
188
193
]
189
194
}
190
195
```
191
196
192
- In addition to the types specified in the WebDriver-BiDi
193
- specification, ` SendChannel ` is given first class support with
194
- ` "type": "sendchannel" ` and ` value ` set to the UUID of the
195
- channel. This enables an important pattern: to receive messages from a
196
- remote context, you can send it a ` SendChannel ` object to use for
197
- responses.
198
-
199
- For deserialization, primitive values are converted back to
200
- primitives, but complex values are represented by a ` RemoteObject `
201
- type. In cases like arrays where there is a ` value ` field holding a
202
- container object, ` RemoteObject.toLocal() ` recursively converts the
203
- content of the container into local objects (so e.g. a ` type: array ` is
204
- converted into a local ` Array ` instances, and any contained ` type: object `
205
- objects are converted into local ` Object ` instances, and so on through
206
- the full tree).
197
+ This supports the following types:
198
+
199
+ * All JS primitive types
200
+ * Builtin types: ` Date ` , ` Regexp ` , ` Error `
201
+ * Functions
202
+ * Collections: ` Array ` , ` Map ` , ` Set ` , ` Object `
203
+ * ` SendChannel ` . This enables an important pattern: to receive
204
+ messages from a remote context, you can send it a ` SendChannel `
205
+ object to use for responses.
206
+ * ` RemoteObject ` . This enables sending an arbitrary object as a
207
+ reference that can be resolved in the original realm. For example an
208
+ ` Element ` named ` elem ` can be transferred as a
209
+ ` RemoteObject(elem) ` . If this ` RemoteObject ` is later transferred
210
+ back to the original realm it will be converted back to the original
211
+ object.
212
+
213
+ Deserialization creates values equivalent to the original values in
214
+ the current realm e.g. a serialized array is reconstructed as an array
215
+ object in the realm where deserialization occurs, and similarly for
216
+ each element in the array. ` RemoteObject ` differs slightly; given a
217
+ serialized ` RemoteObject ` referencing some object ` obj ` , if ` obj `
218
+ doesn't exist in the current realm a new ` RemoteObject ` is created
219
+ referencing ` obj ` . If ` obj ` does exist in that realm, it is returned
220
+ as the result of deserialization.
207
221
208
222
#### Higher Level API
209
223
@@ -221,11 +235,16 @@ send messages to the remote. Alternatively the `RemoteWindow` may be
221
235
created first and its ` uuid ` property used when constructing the URL.
222
236
223
237
Inside the remote browsing context itself, the test author has to call
224
- ` await start_window() ` in order to set up a ` RecvChannel ` with UUID given
225
- by the ` uuid ` parameter in ` location.href ` . The returned object offers
226
- an ` addMessageHandler(callback) ` API to receive messages sent with the
227
- ` postMessage ` API on ` RemoteWindow ` , and ` async nextMessage() ` to wait
228
- for the next message.
238
+ ` window_channel() ` in order to set up a ` RecvChannel ` with UUID given
239
+ by the ` uuid ` parameter in ` location.href ` . By default this is not
240
+ connected until the ` async connect() ` method is called. This allows
241
+ message handlers to be attached before processing any messages. For
242
+ convenience ` await start_window_channel() ` returns an already
243
+ connected ` RecvChannel ` .
244
+
245
+ The ` RecvChannel ` object offers an ` addMessageHandler(callback) ` API
246
+ to receive messages sent with the ` postMessage ` API on ` RemoteWindow ` ,
247
+ and ` async nextMessage() ` to wait for the next message.
229
248
230
249
##### Script Execution
231
250
@@ -242,20 +261,13 @@ execution results in a `Promise` value, the result of that promise is
242
261
awaited. The final return value after the promise is resolved is sent
243
262
back and forms the async return value of the ` executeScript ` call. If
244
263
the script throws, the thrown value is provided as the result, and
245
- re-thrown in the originating context. In addition an ` exceptionDetails `
246
- field provides the line/column numbers of the original exception,
247
- where available.
248
-
249
- In addition there is a `RemoteWindow.executeScriptNoResult(fn,
250
- ...args)` method. This works the same way except no channel is passed,
251
- and so no result is returned. This can be useful in case the script
252
- does something like trigger a navigation, so there's no need to
253
- synchronize the navigation starting (which will close the socket) with
254
- writing the response.
264
+ re-thrown in the originating context. In addition an
265
+ ` exceptionDetails ` field on the response provides the line/column
266
+ numbers of the original exception, where available.
255
267
256
268
TODO: the naming here isn't great. In particular a ` RemoteWindow `
257
269
could actually be some other kind of global like a worker, and
258
- ` start_window ()` is a pretty nondescript method name.
270
+ ` start_window_channel ()` is a pretty nondescript method name.
259
271
260
272
#### Navigation and bfcache
261
273
@@ -374,10 +386,19 @@ class RecvChannel() {
374
386
async next(): Promise <Object > {}
375
387
}
376
388
389
+ /**
390
+ * Create an unconnected channel defined by a `uuid` in
391
+ * `location.href` for listening for RemoteWindow messages.
392
+ */
393
+ async window_channel (): RemoteWindowCommandRecvChannel {}
394
+
395
+
377
396
/**
378
- * Start listening for RemoteWindow messages on a channel defined by a `uuid` in `location.href`
397
+ * Start listening for RemoteWindow messages on a channel defined by
398
+ * a `uuid` in `location.href`
379
399
*/
380
- async start_window (): Promise < RemoteWindowCommandRecvChannel > {}
400
+ async start_window_channel (): Promise < RemoteWindowCommandRecvChannel > {}
401
+
381
402
382
403
/**
383
404
* Handler for RemoteWindow commands
@@ -449,31 +470,32 @@ class RemoteWindow {
449
470
* Arguments and return values are serialized as RemoteObjects.
450
471
*/
451
472
async executeScript(fn : (args : ...any ) => any , ..args: any ): Promise <any > {}
452
-
453
- /**
454
- * Run the function `fn` in the remote context, passing arguments
455
- * `args`, but without returning a result
456
- *
457
- * Arguments are serialized as RemoteObjects.
458
- */
459
- async executeScriptNoResult(fn : (args : ...any ) => any , ..args: any ) {}
460
473
}
461
474
462
475
/**
463
476
* Representation of a non-primitive type passed through a channel
464
477
*/
465
478
class RemoteObject {
466
479
type: string ;
467
- value: any ;
468
480
objectId: string | undefined ;
469
481
470
482
/**
471
- * Recursively convert the object to a local type (where possible)
472
- * so eg. a remote array is converted into a local array.
473
- *
474
- * Objects without a meaningful local representation are passed back unchanged.
483
+ * Create a RemoteObject containing a handle to reference obj
484
+ */
485
+ static from(obj ): RemoteObject
486
+
487
+ /**
488
+ * Return the local object referenced by the objectId of this RemoteObject,
489
+ * or null if there isn't a such an object in this realm.
490
+ */
491
+ toLocal(): Object ?
492
+
493
+ /**
494
+ * Remove the objectId from the local cache. This means that future
495
+ * calls to `toLocal` with the same objectId will always return
496
+ * `null`.
475
497
*/
476
- toLocal() : any {}
498
+ delete()
477
499
}
478
500
479
501
/**
@@ -536,12 +558,23 @@ child.html
536
558
<p id =" nottest" >FAIL</p >
537
559
<p id =" test" >PASS</p >
538
560
<script >
539
- start_window ();
561
+ start_window_channel ();
540
562
</script >
541
563
```
542
564
565
+ ## Implementation Requirements
566
+
567
+ The implementation only depends on wptserve and normal content js; it
568
+ doesn't depend on testdriver or any test-only APIs. Therefore
569
+ integration into any deployment not using wptrunner/testdriver should
570
+ be straightforward.
571
+
543
572
## Possible Future Additions
544
573
574
+ Note: Implementation of any suggestions in this section would happen
575
+ in the context of a further RFC. This section is only to sketch some
576
+ possibilities for further development of the API.
577
+
545
578
The primitives here could be integrated more completely with
546
579
testharness.js. For example we could use a ` RemoteWindow ` as a source
547
580
of tests in ` fetch_tests_from_window ` . Alternatively, or in addition
@@ -571,10 +604,11 @@ for websockets. By sticking close to WebDriver BiDi proposed semantics
571
604
the transition may even be seamless.
572
605
573
606
testdriver integration is possible. For example we could add
574
- ` RemoteContext.testdriver.click ` to execute a click in the remote
575
- context (and similarly for the remainder of the testdriver
576
- API). testdriver in this case would identify the target window by
577
- looking for a window with the appropriate ` uuid ` parameter in its
607
+ ` RemoteContext.testdriver.set_permission(params) ` to execute a
608
+ ` set_permission ` command in the remote context (and similarly for the
609
+ remainder of the testdriver API). This would desugar to
610
+ ` testdriver.set_permission(params, uuid) ` and testdriver would be
611
+ update to identify the target window from the ` uuid ` parameter in its
578
612
` location.href ` .
579
613
580
614
## Risks
0 commit comments