tree: dcaaca289288342376f4f39dcc1b0a289b7213b6 [path history] [tgz]
  1. dispatcher.js
  2. dispatcher.py
  3. executor-service-worker.js
  4. executor-worker.js
  5. executor.html
  6. README.md
  7. w3c-import.log
LayoutTests/imported/w3c/web-platform-tests/common/dispatcher/README.md

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:

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 and executor.html in back-forward cache tests. Note that executor files under /common/dispatcher/ are NOT for RemoteContext.execute_script().

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.

RemoteContext.execute_script() calls should be serialized by always waiting for the returned promise to be resolved. So it's a good practice to always write await ctx.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.

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 is still under discussion.