Upload Kmake

This commit is contained in:
Gorochu
2026-05-26 23:36:42 -07:00
parent ba051b2f74
commit 555ec72358
41615 changed files with 13344630 additions and 1 deletions

3
test/fixtures/wpt/common/META.yml vendored Normal file
View File

@ -0,0 +1,3 @@
suggested_reviewers:
- zqzhang
- deniak

View File

@ -0,0 +1,116 @@
/**
* Supports pseudo-"namespacing" localStorage for a given test
* by generating and using a unique prefix for keys. Why trounce on other
* tests' localStorage items when you can keep it "separated"?
*
* PrefixedLocalStorageTest: Instantiate in testharness.js tests to generate
* a new unique-ish prefix
* PrefixedLocalStorageResource: Instantiate in supporting test resource
* files to use/share a prefix generated by a test.
*/
var PrefixedLocalStorage = function () {
this.prefix = ''; // Prefix for localStorage keys
this.param = 'prefixedLocalStorage'; // Param to use in querystrings
};
PrefixedLocalStorage.prototype.clear = function () {
if (this.prefix === '') { return; }
Object.keys(localStorage).forEach(sKey => {
if (sKey.indexOf(this.prefix) === 0) {
localStorage.removeItem(sKey);
}
});
};
/**
* Append/replace prefix parameter and value in URI querystring
* Use to generate URLs to resource files that will share the prefix.
*/
PrefixedLocalStorage.prototype.url = function (uri) {
function updateUrlParameter (uri, key, value) {
var i = uri.indexOf('#');
var hash = (i === -1) ? '' : uri.substr(i);
uri = (i === -1) ? uri : uri.substr(0, i);
var re = new RegExp(`([?&])${key}=.*?(&|$)`, 'i');
var separator = uri.indexOf('?') !== -1 ? '&' : '?';
uri = (uri.match(re)) ? uri.replace(re, `$1${key}=${value}$2`) :
`${uri}${separator}${key}=${value}`;
return uri + hash;
}
return updateUrlParameter(uri, this.param, this.prefix);
};
PrefixedLocalStorage.prototype.prefixedKey = function (baseKey) {
return `${this.prefix}${baseKey}`;
};
PrefixedLocalStorage.prototype.setItem = function (baseKey, value) {
localStorage.setItem(this.prefixedKey(baseKey), value);
};
/**
* Listen for `storage` events pertaining to a particular key,
* prefixed with this object's prefix. Ignore when value is being set to null
* (i.e. removeItem).
*/
PrefixedLocalStorage.prototype.onSet = function (baseKey, fn) {
window.addEventListener('storage', e => {
var match = this.prefixedKey(baseKey);
if (e.newValue !== null && e.key.indexOf(match) === 0) {
fn.call(this, e);
}
});
};
/*****************************************************************************
* Use in a testharnessjs test to generate a new key prefix.
* async_test(t => {
* var prefixedStorage = new PrefixedLocalStorageTest();
* t.add_cleanup(() => prefixedStorage.cleanup());
* /...
* });
*/
var PrefixedLocalStorageTest = function () {
PrefixedLocalStorage.call(this);
this.prefix = `${document.location.pathname}-${Math.random()}-${Date.now()}-`;
};
PrefixedLocalStorageTest.prototype = Object.create(PrefixedLocalStorage.prototype);
PrefixedLocalStorageTest.prototype.constructor = PrefixedLocalStorageTest;
/**
* Use in a cleanup function to clear out prefixed entries in localStorage
*/
PrefixedLocalStorageTest.prototype.cleanup = function () {
this.setItem('closeAll', 'true');
this.clear();
};
/*****************************************************************************
* Use in test resource files to share a prefix generated by a
* PrefixedLocalStorageTest. Will look in URL querystring for prefix.
* Setting `close_on_cleanup` opt truthy will make this script's window listen
* for storage `closeAll` event from controlling test and close itself.
*
* var PrefixedLocalStorageResource({ close_on_cleanup: true });
*/
var PrefixedLocalStorageResource = function (options) {
PrefixedLocalStorage.call(this);
this.options = Object.assign({}, {
close_on_cleanup: false
}, options || {});
// Check URL querystring for prefix to use
var regex = new RegExp(`[?&]${this.param}(=([^&#]*)|&|#|$)`),
results = regex.exec(document.location.href);
if (results && results[2]) {
this.prefix = results[2];
}
// Optionally have this window close itself when the PrefixedLocalStorageTest
// sets a `closeAll` item.
if (this.options.close_on_cleanup) {
this.onSet('closeAll', () => {
window.close();
});
}
};
PrefixedLocalStorageResource.prototype = Object.create(PrefixedLocalStorage.prototype);
PrefixedLocalStorageResource.prototype.constructor = PrefixedLocalStorageResource;

View File

@ -0,0 +1 @@
Content-Type: text/javascript; charset=utf-8

View File

@ -0,0 +1,100 @@
/**
* Supports pseudo-"namespacing" for window-posted messages for a given test
* by generating and using a unique prefix that gets wrapped into message
* objects. This makes it more feasible to have multiple tests that use
* `window.postMessage` in a single test file. Basically, make it possible
* for the each test to listen for only the messages that are pertinent to it.
*
* 'Prefix' not an elegant term to use here but this models itself after
* PrefixedLocalStorage.
*
* PrefixedMessageTest: Instantiate in testharness.js tests to generate
* a new unique-ish prefix that can be used by other test support files
* PrefixedMessageResource: Instantiate in supporting test resource
* files to use/share a prefix generated by a test.
*/
var PrefixedMessage = function () {
this.prefix = '';
this.param = 'prefixedMessage'; // Param to use in querystrings
};
/**
* Generate a URL that adds/replaces param with this object's prefix
* Use to link to test support files that make use of
* PrefixedMessageResource.
*/
PrefixedMessage.prototype.url = function (uri) {
function updateUrlParameter (uri, key, value) {
var i = uri.indexOf('#');
var hash = (i === -1) ? '' : uri.substr(i);
uri = (i === -1) ? uri : uri.substr(0, i);
var re = new RegExp(`([?&])${key}=.*?(&|$)`, 'i');
var separator = uri.indexOf('?') !== -1 ? '&' : '?';
uri = (uri.match(re)) ? uri.replace(re, `$1${key}=${value}$2`) :
`${uri}${separator}${key}=${value}`;
return uri + hash;
}
return updateUrlParameter(uri, this.param, this.prefix);
};
/**
* Add an eventListener on `message` but only invoke the given callback
* for messages whose object contains this object's prefix. Remove the
* event listener once the anticipated message has been received.
*/
PrefixedMessage.prototype.onMessage = function (fn) {
window.addEventListener('message', e => {
if (typeof e.data === 'object' && e.data.hasOwnProperty('prefix')) {
if (e.data.prefix === this.prefix) {
// Only invoke callback when `data` is an object containing
// a `prefix` key with this object's prefix value
// Note fn is invoked with "unwrapped" data first, then the event `e`
// (which contains the full, wrapped e.data should it be needed)
fn.call(this, e.data.data, e);
window.removeEventListener('message', fn);
}
}
});
};
/**
* Instantiate in a test file (e.g. during `setup`) to create a unique-ish
* prefix that can be shared by support files
*/
var PrefixedMessageTest = function () {
PrefixedMessage.call(this);
this.prefix = `${document.location.pathname}-${Math.random()}-${Date.now()}-`;
};
PrefixedMessageTest.prototype = Object.create(PrefixedMessage.prototype);
PrefixedMessageTest.prototype.constructor = PrefixedMessageTest;
/**
* Instantiate in a test support script to use a "prefix" generated by a
* PrefixedMessageTest in a controlling test file. It will look for
* the prefix in a URL param (see also PrefixedMessage#url)
*/
var PrefixedMessageResource = function () {
PrefixedMessage.call(this);
// Check URL querystring for prefix to use
var regex = new RegExp(`[?&]${this.param}(=([^&#]*)|&|#|$)`),
results = regex.exec(document.location.href);
if (results && results[2]) {
this.prefix = results[2];
}
};
PrefixedMessageResource.prototype = Object.create(PrefixedMessage.prototype);
PrefixedMessageResource.prototype.constructor = PrefixedMessageResource;
/**
* This is how a test resource document can "send info" to its
* opener context. It will whatever message is being sent (`data`) in
* an object that injects the prefix.
*/
PrefixedMessageResource.prototype.postToOpener = function (data) {
if (window.opener) {
window.opener.postMessage({
prefix: this.prefix,
data: data
}, '*');
}
};

View File

@ -0,0 +1 @@
Content-Type: text/javascript; charset=utf-8

10
test/fixtures/wpt/common/README.md vendored Normal file
View File

@ -0,0 +1,10 @@
The files in this directory are non-infrastructure support files that can be used by tests.
* `blank.html` - An empty HTML document.
* `domain-setter.sub.html` - An HTML document that sets `document.domain`.
* `dummy.xhtml` - An XHTML document.
* `dummy.xml` - An XML document.
* `text-plain.txt` - A text/plain document.
* `*.js` - Utility scripts. These are documented in the source.
* `*.py` - wptserve [Python Handlers](https://web-platform-tests.org/writing-tests/python-handlers/). These are documented in the source.
* `security-features` - Documented in `security-features/README.md`.

31
test/fixtures/wpt/common/arrays.js vendored Normal file
View File

@ -0,0 +1,31 @@
/**
* Callback for checking equality of c and d.
*
* @callback equalityCallback
* @param {*} c
* @param {*} d
* @returns {boolean}
*/
/**
* Returns true if the given arrays are equal. Optionally can pass an equality function.
* @param {Array} a
* @param {Array} b
* @param {equalityCallback} callbackFunction - defaults to `c === d`
* @returns {boolean}
*/
export function areArraysEqual(a, b, equalityFunction = (c, d) => { return c === d; }) {
try {
if (a.length !== b.length)
return false;
for (let i = 0; i < a.length; i++) {
if (!equalityFunction(a[i], b[i]))
return false;
}
} catch (ex) {
return false;
}
return true;
}

View File

View File

@ -0,0 +1 @@
Access-Control-Allow-Origin: *

0
test/fixtures/wpt/common/blank.html vendored Normal file
View File

View File

@ -0,0 +1,32 @@
const custom_cors_response = (payload, base_url) => {
base_url = base_url || new URL(location.href);
// Clone the given `payload` so that, as we modify it, we won't be mutating
// the caller's value in unexpected ways.
payload = Object.assign({}, payload);
payload.headers = payload.headers || {};
// Note that, in order to have out-of-the-box support for tests that don't
// call `setup({'allow_uncaught_exception': true})` we return a no-op JS
// payload. This approach will avoid hitting syntax errors if the resource is
// interpreted as script. Without this workaround, the SyntaxError would be
// caught by the test harness and trigger a test failure.
payload.content = payload.content || '/* custom-cors-response.js content */';
payload.status_code = payload.status_code || 200;
// Assume that we'll be doing a CORS-enabled fetch so we'll need to set ACAO.
const acao = "Access-Control-Allow-Origin";
if (!(acao in payload.headers)) {
payload.headers[acao] = '*';
}
if (!("Content-Type" in payload.headers)) {
payload.headers["Content-Type"] = "text/javascript";
}
let ret = new URL("/common/CustomCorsResponse.py", base_url);
for (const key in payload) {
ret.searchParams.append(key, JSON.stringify(payload[key]));
}
return ret;
};

View File

@ -0,0 +1,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.

View File

@ -0,0 +1,256 @@
// Define a universal message passing API. It works cross-origin and across
// browsing context groups.
const dispatcher_path = "/common/dispatcher/dispatcher.py";
const dispatcher_url = new URL(dispatcher_path, location.href).href;
// Return a promise, limiting the number of concurrent accesses to a shared
// resources to |max_concurrent_access|.
const concurrencyLimiter = (max_concurrency) => {
let pending = 0;
let waiting = [];
return async (task) => {
pending++;
if (pending > max_concurrency)
await new Promise(resolve => waiting.push(resolve));
let result = await task();
pending--;
waiting.shift()?.();
return result;
};
}
// Wait for a random amount of time in the range [10ms,100ms].
const randomDelay = () => {
return new Promise(resolve => setTimeout(resolve, 10 + 90*Math.random()));
}
// Sending too many requests in parallel causes congestion. Limiting it improves
// throughput.
//
// Note: The following table has been determined on the test:
// ../cache-storage.tentative.https.html
// using Chrome with a 64 core CPU / 64GB ram, in release mode:
// ┌───────────┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬────┐
// │concurrency│ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │ 10│ 15│ 20│ 30│ 50│ 100│
// ├───────────┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼────┤
// │time (s) │ 54│ 38│ 31│ 29│ 26│ 24│ 22│ 22│ 22│ 22│ 34│ 36 │
// └───────────┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴────┘
const limiter = concurrencyLimiter(6);
// While requests to different remote contexts can go in parallel, we need to
// ensure that requests to each remote context are done in order. This maps a
// uuid to a queue of requests to send. A queue is processed until it is empty
// and then is deleted from the map.
const sendQueues = new Map();
// Sends a single item (with rate-limiting) and calls the associated resolver
// when it is successfully sent.
const sendItem = async function (uuid, resolver, message) {
await limiter(async () => {
// Requests might be dropped. Retry until getting a confirmation it has been
// processed.
while(1) {
try {
let response = await fetch(dispatcher_url + `?uuid=${uuid}`, {
method: 'POST',
body: message
})
if (await response.text() == "done") {
resolver();
return;
}
} catch (fetch_error) {}
await randomDelay();
};
});
}
// While the queue is non-empty, send the next item. This is async and new items
// may be added to the queue while others are being sent.
const processQueue = async function (uuid, queue) {
while (queue.length) {
const [resolver, message] = queue.shift();
await sendItem(uuid, resolver, message);
}
// The queue is empty, delete it.
sendQueues.delete(uuid);
}
const send = async function (uuid, message) {
const itemSentPromise = new Promise((resolve) => {
const item = [resolve, message];
if (sendQueues.has(uuid)) {
// There is already a queue for `uuid`, just add to it and it will be processed.
sendQueues.get(uuid).push(item);
} else {
// There is no queue for `uuid`, create it and start processing.
const queue = [item];
sendQueues.set(uuid, queue);
processQueue(uuid, queue);
}
});
// Wait until the item has been successfully sent.
await itemSentPromise;
}
const receive = async function (uuid) {
while(1) {
let data = "not ready";
try {
data = await limiter(async () => {
let response = await fetch(dispatcher_url + `?uuid=${uuid}`);
return await response.text();
});
} catch (fetch_error) {}
if (data == "not ready") {
await randomDelay();
continue;
}
return data;
}
}
// Returns an URL. When called, the server sends toward the `uuid` queue the
// request headers. Useful for determining if something was requested with
// Cookies.
const showRequestHeaders = function(origin, uuid) {
return origin + dispatcher_path + `?uuid=${uuid}&show-headers`;
}
// Same as above, except for the response is cacheable.
const cacheableShowRequestHeaders = function(origin, uuid) {
return origin + dispatcher_path + `?uuid=${uuid}&cacheable&show-headers`;
}
// This script requires
// - `/common/utils.js` for `token()`.
// Returns the URL of a document that can be used as a `RemoteContext`.
//
// `uuid` should be a UUID uniquely identifying the given remote context.
// `options` has the following shape:
//
// {
// host: (optional) Sets the returned URL's `host` property. Useful for
// cross-origin executors.
// protocol: (optional) Sets the returned URL's `protocol` property.
// }
function remoteExecutorUrl(uuid, options) {
const url = new URL("/common/dispatcher/remote-executor.html", location);
url.searchParams.set("uuid", uuid);
if (options?.host) {
url.host = options.host;
}
if (options?.protocol) {
url.protocol = options.protocol;
}
return url;
}
// Represents a remote executor. For more detailed explanation see `README.md`.
class RemoteContext {
// `uuid` is a UUID string that identifies the remote context and should
// match with the `uuid` parameter of the URL of the remote context.
constructor(uuid) {
this.context_id = uuid;
}
// Evaluates the script `expr` on the executor.
// - If `expr` is evaluated to a Promise that is resolved with a value:
// `execute_script()` returns a Promise resolved with the value.
// - If `expr` is evaluated to a non-Promise value:
// `execute_script()` returns a Promise resolved with the value.
// - If `expr` throws an error or is evaluated to a Promise that is rejected:
// `execute_script()` returns a rejected Promise with the error's
// `message`.
// Note that currently the type of error (e.g. DOMException) is not
// preserved, except for `TypeError`.
// The values should be able to be serialized by JSON.stringify().
async execute_script(fn, args) {
const receiver = token();
await this.send({receiver: receiver, fn: fn.toString(), args: args});
const response = JSON.parse(await receive(receiver));
if (response.status === 'success') {
return response.value;
}
// exception
if (response.name === 'TypeError') {
throw new TypeError(response.value);
}
throw new Error(response.value);
}
async send(msg) {
return await send(this.context_id, JSON.stringify(msg));
}
};
class Executor {
constructor(uuid) {
this.uuid = uuid;
// If `suspend_callback` is not `null`, the executor should be suspended
// when there are no ongoing tasks.
this.suspend_callback = null;
this.execute();
}
// Wait until there are no ongoing tasks nor fetch requests for polling
// tasks, and then suspend the executor and call `callback()`.
// Navigation from the executor page should be triggered inside `callback()`,
// to avoid conflict with in-flight fetch requests.
suspend(callback) {
this.suspend_callback = callback;
}
resume() {
}
async execute() {
while(true) {
if (this.suspend_callback !== null) {
this.suspend_callback();
this.suspend_callback = null;
// Wait for `resume()` to be called.
await new Promise(resolve => this.resume = resolve);
// Workaround for https://crbug.com/1244230.
// Without this workaround, the executor is resumed and the fetch
// request to poll the next task is initiated synchronously from
// pageshow event after the page restored from BFCache, and the fetch
// request promise is never resolved (and thus the test results in
// timeout) due to https://crbug.com/1244230. The root cause is not yet
// known, but setTimeout() with 0ms causes the resume triggered on
// another task and seems to resolve the issue.
await new Promise(resolve => setTimeout(resolve, 0));
continue;
}
const task = JSON.parse(await receive(this.uuid));
let response;
try {
const value = await eval(task.fn).apply(null, task.args);
response = JSON.stringify({
status: 'success',
value: value
});
} catch(e) {
response = JSON.stringify({
status: 'exception',
name: e.name,
value: e.message
});
}
await send(task.receiver, response);
}
}
}

View File

@ -0,0 +1,24 @@
importScripts('./dispatcher.js');
const params = new URLSearchParams(location.search);
const uuid = params.get('uuid');
// The fetch handler must be registered before parsing the main script response.
// So do it here, for future use.
fetchHandler = () => {}
addEventListener('fetch', e => {
fetchHandler(e);
});
// Force ServiceWorker to immediately activate itself.
addEventListener('install', event => {
skipWaiting();
});
let executeOrders = async function() {
while(true) {
let task = await receive(uuid);
eval(`(async () => {${task}})()`);
}
};
executeOrders();

View File

@ -0,0 +1,12 @@
importScripts('./dispatcher.js');
const params = new URLSearchParams(location.search);
const uuid = params.get('uuid');
let executeOrders = async function() {
while(true) {
let task = await receive(uuid);
eval(`(async () => {${task}})()`);
}
};
executeOrders();

View File

@ -0,0 +1,15 @@
<script src="./dispatcher.js"></script>
<script>
const params = new URLSearchParams(window.location.search);
const uuid = params.get('uuid');
let executeOrders = async function() {
while(true) {
let task = await receive(uuid);
eval(`(async () => {${task}})()`);
}
};
executeOrders();
</script>

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<meta charset="utf-8">
<body>
</body>
<script src="./dispatcher.js"></script>
<script>
const params = new URLSearchParams(window.location.search);
const uuid = params.get('uuid');
const executor = new Executor(uuid); // `execute()` is called in constructor.
</script>
</html>

View File

@ -0,0 +1,8 @@
<!DOCTYPE html>
<meta charset="utf-8">
<title>A page that will likely be same-origin-domain but not same-origin</title>
<script>
"use strict";
document.domain = "{{host}}";
</script>

2
test/fixtures/wpt/common/dummy.xhtml vendored Normal file
View File

@ -0,0 +1,2 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"><head><title>Dummy XHTML document</title></head><body /></html>

1
test/fixtures/wpt/common/dummy.xml vendored Normal file
View File

@ -0,0 +1 @@
<foo>Dummy XML document</foo>

52
test/fixtures/wpt/common/gc.js vendored Normal file
View File

@ -0,0 +1,52 @@
/**
* Does a best-effort attempt at invoking garbage collection. Attempts to use
* the standardized `TestUtils.gc()` function, but falls back to other
* environment-specific nonstandard functions, with a final result of just
* creating a lot of garbage (in which case you will get a console warning).
*
* This should generally only be used to attempt to trigger bugs and crashes
* inside tests, i.e. cases where if garbage collection happened, then this
* should not trigger some misbehavior. You cannot rely on garbage collection
* successfully trigger, or that any particular unreachable object will be
* collected.
*
* @returns {Promise<undefined>} A promise you should await to ensure garbage
* collection has had a chance to complete.
*/
self.garbageCollect = async () => {
// https://testutils.spec.whatwg.org/#the-testutils-namespace
if (self.TestUtils?.gc) {
return TestUtils.gc();
}
// Use --expose_gc for V8 (and Node.js)
// to pass this flag at chrome launch use: --js-flags="--expose-gc"
// Exposed in SpiderMonkey shell as well
if (self.gc) {
return self.gc();
}
// Present in some WebKit development environments
if (self.GCController) {
return GCController.collect();
}
console.warn(
'Tests are running without the ability to do manual garbage collection. ' +
'They will still work, but coverage will be suboptimal.');
for (var i = 0; i < 1000; i++) {
gcRec(10);
}
function gcRec(n) {
if (n < 1) {
return {};
}
let temp = { i: "ab" + i + i / 100000 };
temp += "foo";
gcRec(n - 1);
}
};

View File

@ -0,0 +1,63 @@
/**
* Host information for cross-origin tests.
* @returns {Object} with properties for different host information.
*/
function get_host_info() {
var HTTP_PORT = '{{ports[http][0]}}';
var HTTP_PORT2 = '{{ports[http][1]}}';
var HTTPS_PORT = '{{ports[https][0]}}';
var HTTPS_PORT2 = '{{ports[https][1]}}';
var PROTOCOL = self.location.protocol;
var IS_HTTPS = (PROTOCOL == "https:");
var PORT = IS_HTTPS ? HTTPS_PORT : HTTP_PORT;
var PORT2 = IS_HTTPS ? HTTPS_PORT2 : HTTP_PORT2;
var HTTP_PORT_ELIDED = HTTP_PORT == "80" ? "" : (":" + HTTP_PORT);
var HTTP_PORT2_ELIDED = HTTP_PORT2 == "80" ? "" : (":" + HTTP_PORT2);
var HTTPS_PORT_ELIDED = HTTPS_PORT == "443" ? "" : (":" + HTTPS_PORT);
var PORT_ELIDED = IS_HTTPS ? HTTPS_PORT_ELIDED : HTTP_PORT_ELIDED;
var ORIGINAL_HOST = '{{host}}';
var REMOTE_HOST = (ORIGINAL_HOST === 'localhost') ? '127.0.0.1' : ('www1.' + ORIGINAL_HOST);
var OTHER_HOST = '{{domains[www2]}}';
var NOTSAMESITE_HOST = (ORIGINAL_HOST === 'localhost') ? '127.0.0.1' : ('{{hosts[alt][]}}');
return {
HTTP_PORT: HTTP_PORT,
HTTP_PORT2: HTTP_PORT2,
HTTPS_PORT: HTTPS_PORT,
HTTPS_PORT2: HTTPS_PORT2,
PORT: PORT,
PORT2: PORT2,
ORIGINAL_HOST: ORIGINAL_HOST,
REMOTE_HOST: REMOTE_HOST,
ORIGIN: PROTOCOL + "//" + ORIGINAL_HOST + PORT_ELIDED,
HTTP_ORIGIN: 'http://' + ORIGINAL_HOST + HTTP_PORT_ELIDED,
HTTPS_ORIGIN: 'https://' + ORIGINAL_HOST + HTTPS_PORT_ELIDED,
HTTPS_ORIGIN_WITH_CREDS: 'https://foo:bar@' + ORIGINAL_HOST + HTTPS_PORT_ELIDED,
HTTP_ORIGIN_WITH_DIFFERENT_PORT: 'http://' + ORIGINAL_HOST + HTTP_PORT2_ELIDED,
REMOTE_ORIGIN: PROTOCOL + "//" + REMOTE_HOST + PORT_ELIDED,
OTHER_ORIGIN: PROTOCOL + "//" + OTHER_HOST + PORT_ELIDED,
HTTP_REMOTE_ORIGIN: 'http://' + REMOTE_HOST + HTTP_PORT_ELIDED,
HTTP_NOTSAMESITE_ORIGIN: 'http://' + NOTSAMESITE_HOST + HTTP_PORT_ELIDED,
HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT: 'http://' + REMOTE_HOST + HTTP_PORT2_ELIDED,
HTTPS_REMOTE_ORIGIN: 'https://' + REMOTE_HOST + HTTPS_PORT_ELIDED,
HTTPS_REMOTE_ORIGIN_WITH_CREDS: 'https://foo:bar@' + REMOTE_HOST + HTTPS_PORT_ELIDED,
HTTPS_NOTSAMESITE_ORIGIN: 'https://' + NOTSAMESITE_HOST + HTTPS_PORT_ELIDED,
UNAUTHENTICATED_ORIGIN: 'http://' + OTHER_HOST + HTTP_PORT_ELIDED,
AUTHENTICATED_ORIGIN: 'https://' + OTHER_HOST + HTTPS_PORT_ELIDED
};
}
/**
* When a default port is used, location.port returns the empty string.
* This function attempts to provide an exact port, assuming we are running under wptserve.
* @param {*} loc - can be Location/<a>/<area>/URL, but assumes http/https only.
* @returns {string} The port number.
*/
function get_port(loc) {
if (loc.port) {
return loc.port;
}
return loc.protocol === 'https:' ? '443' : '80';
}

View File

@ -0,0 +1 @@
Content-Type: text/javascript; charset=utf-8

55
test/fixtures/wpt/common/media.js vendored Normal file
View File

@ -0,0 +1,55 @@
/**
* Returns the URL of a supported video source based on the user agent
* @param {string} base - media URL without file extension
* @returns {string}
*/
function getVideoURI(base)
{
var extension = '.mp4';
var videotag = document.createElement("video");
if ( videotag.canPlayType &&
videotag.canPlayType('video/ogg; codecs="theora, vorbis"') )
{
extension = '.ogv';
}
return base + extension;
}
/**
* Returns the URL of a supported audio source based on the user agent
* @param {string} base - media URL without file extension
* @returns {string}
*/
function getAudioURI(base)
{
var extension = '.mp3';
var audiotag = document.createElement("audio");
if ( audiotag.canPlayType &&
audiotag.canPlayType('audio/ogg') )
{
extension = '.oga';
}
return base + extension;
}
/**
* Returns the MIME type for a media URL based on the file extension.
* @param {string} url
* @returns {string}
*/
function getMediaContentType(url) {
var extension = new URL(url, location).pathname.split(".").pop();
var map = {
"mp4": "video/mp4",
"ogv": "application/ogg",
"mp3": "audio/mp3",
"oga": "application/ogg",
};
return map[extension];
}

View File

@ -0,0 +1 @@
Content-Type: text/javascript; charset=utf-8

View File

@ -0,0 +1,74 @@
"use strict";
// This is for testing whether an object (e.g., a global property) is associated with Window, or
// with Document. Recall that Window and Document are 1:1 except when doing a same-origin navigation
// away from the initial about:blank. In that case the Window object gets reused for the new
// Document.
//
// So:
// - If something is per-Window, then it should maintain its identity across an about:blank
// navigation.
// - If something is per-Document, then it should be recreated across an about:blank navigation.
window.testIsPerWindow = propertyName => {
runTests(propertyName, assert_equals, "must not");
};
window.testIsPerDocument = propertyName => {
runTests(propertyName, assert_not_equals, "must");
};
function runTests(propertyName, equalityOrInequalityAsserter, mustOrMustNotReplace) {
async_test(t => {
const iframe = document.createElement("iframe");
document.body.appendChild(iframe);
const frame = iframe.contentWindow;
const before = frame[propertyName];
assert_implements(before, `window.${propertyName} must be implemented`);
iframe.onload = t.step_func_done(() => {
const after = frame[propertyName];
equalityOrInequalityAsserter(after, before);
});
iframe.src = "/common/blank.html";
}, `Navigating from the initial about:blank ${mustOrMustNotReplace} replace window.${propertyName}`);
// Per spec, discarding a browsing context should not change any of the global objects.
test(() => {
const iframe = document.createElement("iframe");
document.body.appendChild(iframe);
const frame = iframe.contentWindow;
const before = frame[propertyName];
assert_implements(before, `window.${propertyName} must be implemented`);
iframe.remove();
const after = frame[propertyName];
assert_equals(after, before, `window.${propertyName} should not change after iframe.remove()`);
}, `Discarding the browsing context must not change window.${propertyName}`);
// Per spec, document.open() should not change any of the global objects. In historical versions
// of the spec, it did, so we test here.
async_test(t => {
const iframe = document.createElement("iframe");
iframe.onload = t.step_func_done(() => {
const frame = iframe.contentWindow;
const before = frame[propertyName];
assert_implements(before, `window.${propertyName} must be implemented`);
frame.document.open();
const after = frame[propertyName];
assert_equals(after, before);
frame.document.close();
});
iframe.src = "/common/blank.html";
document.body.appendChild(iframe);
}, `document.open() must not replace window.${propertyName}`);
}

View File

@ -0,0 +1 @@
Content-Type: text/javascript; charset=utf-8

View File

@ -0,0 +1,56 @@
/*
author: W3C http://www.w3.org/
help: http://www.w3.org/TR/navigation-timing/#sec-window.performance-attribute
*/
var performanceNamespace = window.performance;
var namespace_check = false;
function wp_test(func, msg, properties)
{
// only run the namespace check once
if (!namespace_check)
{
namespace_check = true;
if (performanceNamespace === undefined || performanceNamespace == null)
{
// show a single error that window.performance is undefined
// The window.performance attribute provides a hosting area for performance related attributes.
test(function() { assert_true(performanceNamespace !== undefined && performanceNamespace != null, "window.performance is defined and not null"); }, "window.performance is defined and not null.");
}
}
test(func, msg, properties);
}
function test_true(value, msg, properties)
{
wp_test(function () { assert_true(value, msg); }, msg, properties);
}
function test_equals(value, equals, msg, properties)
{
wp_test(function () { assert_equals(value, equals, msg); }, msg, properties);
}
// assert for every entry in `expectedEntries`, there is a matching entry _somewhere_ in `actualEntries`
function test_entries(actualEntries, expectedEntries) {
test_equals(actualEntries.length, expectedEntries.length)
expectedEntries.forEach(function (expectedEntry) {
var foundEntry = actualEntries.find(function (actualEntry) {
return typeof Object.keys(expectedEntry).find(function (key) {
return actualEntry[key] !== expectedEntry[key]
}) === 'undefined'
})
test_true(!!foundEntry, `Entry ${JSON.stringify(expectedEntry)} could not be found.`)
if (foundEntry) {
assert_object_equals(foundEntry.toJSON(), expectedEntry)
}
})
}
function delayedLoadListener(callback) {
window.addEventListener('load', function() {
// TODO(cvazac) Remove this setTimeout when spec enforces sync entries.
step_timeout(callback, 0)
})
}

View File

@ -0,0 +1 @@
Content-Type: text/javascript; charset=utf-8

View File

@ -0,0 +1,3 @@
function FindProxyForURL(url, host) {
return "PROXY {{host}}:{{ports[http][0]}}"
}

View File

@ -0,0 +1,39 @@
/**
* Remove the `reftest-wait` class on the document element.
* The reftest runner will wait with taking a screenshot while
* this class is present.
*
* See https://web-platform-tests.org/writing-tests/reftests.html#controlling-when-comparison-occurs
*/
function takeScreenshot() {
document.documentElement.classList.remove("reftest-wait");
}
/**
* Call `takeScreenshot()` after a delay of at least |timeout| milliseconds.
* @param {number} timeout - milliseconds
*/
function takeScreenshotDelayed(timeout) {
setTimeout(function() {
takeScreenshot();
}, timeout);
}
/**
* Ensure that a precondition is met before waiting for a screenshot.
* @param {bool} condition - Fail the test if this evaluates to false
* @param {string} msg - Error message to write to the screenshot
*/
function failIfNot(condition, msg) {
const fail = () => {
(document.body || document.documentElement).textContent = `Precondition Failed: ${msg}`;
takeScreenshot();
};
if (!condition) {
if (document.readyState == "interactive") {
fail();
} else {
document.addEventListener("DOMContentLoaded", fail, false);
}
}
}

View File

@ -0,0 +1 @@
Content-Type: text/javascript; charset=utf-8

View File

@ -0,0 +1,19 @@
"use strict";
/**
* Waits until we have at least one frame rendered, regardless of the engine.
*
* @returns {Promise}
*/
function waitForAtLeastOneFrame() {
return new Promise(resolve => {
// Different web engines work slightly different on this area but waiting
// for two requestAnimationFrames() to happen, one after another, should be
// sufficient to ensure at least one frame has been generated anywhere.
window.requestAnimationFrame(() => {
window.requestAnimationFrame(() => {
resolve();
});
});
});
}

21
test/fixtures/wpt/common/sab.js vendored Normal file
View File

@ -0,0 +1,21 @@
const createBuffer = (() => {
// See https://github.com/whatwg/html/issues/5380 for why not `new SharedArrayBuffer()`
let sabConstructor;
try {
sabConstructor = new WebAssembly.Memory({ shared:true, initial:0, maximum:0 }).buffer.constructor;
} catch(e) {
sabConstructor = null;
}
return (type, length, opts) => {
if (type === "ArrayBuffer") {
return new ArrayBuffer(length, opts);
} else if (type === "SharedArrayBuffer") {
if (sabConstructor && sabConstructor.name !== "SharedArrayBuffer") {
throw new Error("WebAssembly.Memory does not support shared:true");
}
return new sabConstructor(length, opts);
} else {
throw new Error("type has to be ArrayBuffer or SharedArrayBuffer");
}
}
})();

View File

@ -0,0 +1,460 @@
This directory contains the common infrastructure for the following tests (also referred below as projects).
- referrer-policy/
- mixed-content/
- upgrade-insecure-requests/
Subdirectories:
- `resources`:
Serves JavaScript test helpers.
- `subresource`:
Serves subresources, with support for redirects, stash, etc.
The subresource paths are managed by `subresourceMap` and
fetched in `requestVia*()` functions in `resources/common.js`.
- `scope`:
Serves nested contexts, such as iframe documents or workers.
Used from `invokeFrom*()` functions in `resources/common.js`.
- `tools`:
Scripts that generate test HTML files. Not used while running tests.
- `/referrer-policy/generic/subresource-test`:
Sanity checking tests for subresource invocation
(This is still placed outside common/)
# Test generator
The test generator ([common/security-features/tools/generate.py](tools/generate.py)) generates test HTML files from templates and a seed (`spec.src.json`) that defines all the test scenarios.
The project (i.e. a WPT subdirectory, for example `referrer-policy/`) that uses the generator should define per-project data and invoke the common generator logic in `common/security-features/tools`.
This is the overview of the project structure:
```
common/security-features/
└── tools/ - the common test generator logic
├── spec.src.json
└── template/ - the test files templates
project-directory/ (e.g. referrer-policy/)
├── spec.src.json
├── generic/
│ ├── test-case.sub.js - Per-project test helper
│ ├── sanity-checker.js (Used by debug target only)
│ └── spec_json.js (Used by debug target only)
└── gen/ - generated tests
```
## Generating the tests
Note: When the repository already contains generated tests, [remove all generated tests](#removing-all-generated-tests) first.
```bash
# Install json5 module if needed.
pip install --user json5
# Generate the test files under gen/ (HTMLs and .headers files).
path/to/common/security-features/tools/generate.py --spec path/to/project-directory/
# Add all generated tests to the repo.
git add path/to/project-directory/gen/ && git commit -m "Add generated tests"
```
This will parse the spec JSON5 files and determine which tests to generate (or skip) while using templates.
- The default spec JSON5: `common/security-features/tools/spec.src.json`.
- Describes common configurations, such as subresource types, source context types, etc.
- The per-project spec JSON5: `project-directory/spec.src.json`.
- Describes project-specific configurations, particularly those related to test generation patterns (`specification`), policy deliveries (e.g. `delivery_type`, `delivery_value`) and `expectation`.
For how these two spec JSON5 files are merged, see [Sub projects](#sub-projects) section.
Note: `spec.src.json` is transitioning to JSON5 [#21710](https://github.com/web-platform-tests/wpt/issues/21710).
During the generation, the spec is validated by ```common/security-features/tools/spec_validator.py```. This is specially important when you're making changes to `spec.src.json`. Make sure it's a valid JSON (no comments or trailing commas). The validator reports specific errors (missing keys etc.), if any.
### Removing all generated tests
Simply remove all files under `project-directory/gen/`.
```bash
rm -r path/to/project-directory/gen/
```
### Options for generating tests
Note: this section is currently obsolete. Only the release template is working.
The generator script has two targets: ```release``` and ```debug```.
* Using **release** for the target will produce tests using a template for optimizing size and performance. The release template is intended for the official web-platform-tests and possibly other test suites. No sanity checking is done in release mode. Use this option whenever you're checking into web-platform-tests.
* When generating for ```debug```, the produced tests will contain more verbosity and sanity checks. Use this target to identify problems with the test suites when making changes locally. Make sure you don't check in tests generated with the debug target.
Note that **release** is the default target when invoking ```generate.py```.
## Sub projects
Projects can be nested, for example to reuse a single `spec.src.json` across similar but slightly different sets of generated tests.
The directory structure would look like:
```
project-directory/ (e.g. referrer-policy/)
├── spec.src.json - Parent project's spec JSON
├── generic/
│ └── test-case.sub.js - Parent project's test helper
├── gen/ - parent project's generated tests
└── sub-project-directory/ (e.g. 4K)
├── spec.src.json - Child project's spec JSON
├── generic/
│ └── test-case.sub.js - Child project's test helper
└── gen/ - child project's generated tests
```
`generate.py --spec project-directory/sub-project-directory` generates test files under `project-directory/sub-project-directory/gen`, based on `project-directory/spec.src.json` and `project-directory/sub-project-directory/spec.src.json`.
- The child project's `spec.src.json` is merged into parent project's `spec.src.json`.
- Two spec JSON objects are merged recursively.
- If a same key exists in both objects, the child's value overwrites the parent's value.
- If both (child's and parent's) values are arrays, then the child's value is concatenated to the parent's value.
- For debugging, `generate.py` dumps the merged spec JSON object as `generic/debug-output.spec.src.json`.
- The child project's generated tests include both of the parent and child project's `test-case.sub.js`:
```html
<script src="project-directory/test-case.sub.js"></script>
<script src="project-directory/sub-project-directory/test-case.sub.js"></script>
<script>
TestCase(...);
</script>
```
## Updating the tests
The main test logic lives in ```project-directory/generic/test-case.sub.js``` with helper functions defined in ```/common/security-features/resources/common.js``` so you should probably start there.
For updating the test suites you will most likely do **a subset** of the following:
* Add a new subresource type:
* Add a new sub-resource python script to `/common/security-features/subresource/`.
* Add a sanity check test for a sub-resource to `referrer-policy/generic/subresource-test/`.
* Add a new entry to `subresourceMap` in `/common/security-features/resources/common.js`.
* Add a new entry to `valid_subresource_names` in `/common/security-features/tools/spec_validator.py`.
* Add a new entry to `subresource_schema` in `spec.src.json`.
* Update `source_context_schema` to specify in which source context the subresource can be used.
* Add a new subresource redirection type
* TODO: to be documented. Example: [https://github.com/web-platform-tests/wpt/pull/18939](https://github.com/web-platform-tests/wpt/pull/18939)
* Add a new subresource origin type
* TODO: to be documented. Example: [https://github.com/web-platform-tests/wpt/pull/18940](https://github.com/web-platform-tests/wpt/pull/18940)
* Add a new source context (e.g. "module sharedworker global scope")
* TODO: to be documented. Example: [https://github.com/web-platform-tests/wpt/pull/18904](https://github.com/web-platform-tests/wpt/pull/18904)
* Add a new source context list (e.g. "subresource request from a dedicated worker in a `<iframe srcdoc>`")
* TODO: to be documented.
* Implement new or update existing assertions in ```project-directory/generic/test-case.sub.js```.
* Exclude or add some tests by updating ```spec.src.json``` test expansions.
* Implement a new delivery method.
* TODO: to be documented. Currently the support for delivery methods are implemented in many places across `common/security-features/`.
* Regenerate the tests and MANIFEST.json
## How the generator works
This section describes how `spec.src.json` is turned into scenario data in test HTML files which are then processed by JavaScript test helpers and server-side scripts, and describes the objects/types used in the process.
### The spec JSON
`spec.src.json` is the input for the generator that defines what to generate. For examples of spec JSON files, see [referrer-policy/spec.src.json](../../referrer-policy/spec.src.json) or [mixed-content/spec.src.json](../../mixed-content/spec.src.json).
#### Main sections
* **`specification`**
Top level requirements with description fields and a ```test_expansion``` rule.
This is closely mimicking the [Referrer Policy specification](http://w3c.github.io/webappsec/specs/referrer-policy/) structure.
* **`excluded_tests`**
List of ```test_expansion``` patterns expanding into selections which get skipped when generating the tests (aka. blocklisting/suppressing)
* **`test_expansion_schema`**
Provides valid values for each field.
Each test expansion can only contain fields and values defined by this schema (or `"*"` values that indicate all the valid values defined this schema).
* **`subresource_schema`**
Provides metadata of subresources, e.g. supported delivery types for each subresource.
* **`source_context_schema`**
Provides metadata of each single source context, e.g. supported delivery types and subresources that can be sent from the context.
* **`source_context_list_schema`**
Provides possible nested combinations of source contexts. See [SourceContexts Resolution](#sourcecontexts-resolution) section below for details.
### Test Expansion Pattern Object
Test expansion patterns (`test_expansion`s in `specification` section) define the combinations of test configurations (*selections*) to be generated.
Each field in a test expansion can be in one of the following formats:
* Single match: ```"value"```
* Match any of: ```["value1", "value2", ...]```
* Match all: ```"*"```
The following fields have special meaning:
- **`name`**: just ignored. (Previously this was used as a part of filenames but now this is merely a label for human and is never used by generator. This field might be removed in the future (https://github.com/web-platform-tests/wpt/issues/21708))
- **`expansion`**: if there is more than one pattern expanding into a same selection, the pattern appearing later in the spec JSON will overwrite a previously generated selection. To make clear this is intentional, set the value of the ```expansion``` field to ```default``` for an expansion appearing earlier and ```override``` for the one appearing later.
For example a test expansion pattern (taken from [referrer-policy/spec.src.json](../../referrer-policy/spec.src.json), sorted/formatted for explanation):
```json
{
"name": "insecure-protocol",
"expansion": "default",
"delivery_type": "*",
"delivery_value": "no-referrer-when-downgrade",
"source_context_list": "*",
"expectation": "stripped-referrer",
"origin": ["same-http", "cross-http"],
"redirection": "*",
"source_scheme": "http",
"subresource": "*"
}
```
means: "All combinations with all possible `delivery_type`, `delivery_value`=`no-referrer-when-downgrade`, all possible `source_context_list`, `expectation`=`stripped-referrer`, `origin`=`same-http` or `cross-http`, all possible `redirection`, `source_scheme`=`http`, and all possible `subresource`.
### Selection Object
A selection is an object that defines a single test, with keys/values from `test_expansion_schema`.
A single test expansion pattern gets expanded into a list of selections as follows:
* Expand each field's pattern (single, any of, or all) to list of allowed values (defined by the ```test_expansion_schema```)
* Permute - Recursively enumerate all selections across all fields
The following field has special meaning:
- **`delivery_key`**: This doesn't exist in test expansion patterns, and instead is taken from `delivery_key` field of the spec JSON and added into selections. (TODO(https://github.com/web-platform-tests/wpt/issues/21708): probably this should be added to test expansion patterns to remove this special handling)
For example, the test expansion in the example above generates selections like the following selection (which eventually generates [this test file](../../referrer-policy/gen/worker-classic.http-rp/no-referrer-when-downgrade/fetch/same-http.no-redirect.http.html )):
```json
{
"delivery_type": "http-rp",
"delivery_key": "referrerPolicy",
"delivery_value": "no-referrer-when-downgrade",
"source_context_list": "worker-classic",
"expectation": "stripped-referrer",
"origin": "same-http",
"redirection": "no-redirect",
"source_scheme": "http",
"subresource": "fetch"
}
```
### Excluding Test Expansion Patterns
The ```excluded_tests``` section have objects with the same format as [Test Expansion Patterns](#test-expansion-patterns) that define selections to be excluded.
Taking the spec JSON, the generator follows this algorithm:
* Expand all ```excluded_tests``` to create a denylist of selections
* For each `specification` entries: Expand the ```test_expansion``` pattern into selections and check each against the denylist, if not marked as suppresed, generate the test resources for the selection
### SourceContext Resolution
The `source_context_list_schema` section of `spec.src.json` defines templates of policy deliveries and source contexts.
The `source_context_list` value in a selection specifies the key of the template to be used in `source_context_list_schema`, and the following fields in the selection are filled into the template (these three values define the **target policy delivery** to be tested):
- `delivery_type`
- `delivery_key`
- `delivery_value`
#### Source Context List Schema
Each entry of **`source_context_list_schema`**, defines a single template of how/what policies to be delivered in what source contexts (See also [PolicyDelivery](types.md#policydelivery) and [SourceContext](types.md#sourcecontext)).
- The key: the name of the template which matches with the `source_context_list` value in a selection.
- `sourceContextList`: an array of `SourceContext` objects that represents a (possibly nested) context.
- `sourceContextType` of the first entry of `sourceContextList` should be always `"top"`, which represents the top-level generated test HTML. This entry is omitted in the scenario JSON object passed to JavaScript runtime, but the policy deliveries specified here are handled by the generator, e.g. written as `<meta>` elements in the generated test HTML.
- `subresourcePolicyDeliveries`: an array of `PolicyDelivery` objects that represents policies specified at subresource requests (e.g. `referrerPolicy` attribute of `<img>` elements).
#### PolicyDelivery placeholders
Instead to ordinal `PolicyDelivery` objects, the following placeholder strings can be used in `sourceContextList` or `subresourcePolicyDeliveries`.
- `"policy"`:
- Replaced with the target policy delivery.
- `"policyIfNonNull"`:
- Replaced with the target policy delivery, only if `delivery_value` is not `null`.
If `delivery_value` is `null`, then the test is not generated.
- `"anotherPolicy"`:
- Replaced with a `PolicyDelivery` object that has a different value from
the target policy delivery.
- Can be used to specify e.g. a policy that should be overridden by
the target policy delivery.
#### `source_context_schema` and `subresource_schema`
These represent supported delivery types and subresources
for each source context or subresource type. These are used
- To filter out test files for unsupported combinations of delivery types,
source contexts and subresources during SourceContext resolution.
- To determine what delivery types can be used for `anotherPolicy`
placeholder.
#### Example
For example, the following entry in `source_context_list_schema`:
```json
"worker-classic": {
"sourceContextList": [
{
"sourceContextType": "top",
"policyDeliveries": [
"anotherPolicy"
]
},
{
"sourceContextType": "worker-classic",
"policyDeliveries": [
"policy"
]
}
],
"subresourcePolicyDeliveries": []
}
```
Defines a template to be instantiated with `delivery_key`, `delivery_type` and `delivery_value` values defined outside `source_context_list_schema`, which reads:
- A classic `WorkerGlobalScope` is created under the top-level Document, and has a policy defined by `delivery_key`, `delivery_type` and `delivery_value`.
- The top-level Document has a policy different from the policy given to the classic worker (to confirm that the policy of the classic worker, not of the top-level Document, is used).
- The subresource request is sent from the classic `WorkerGlobalScope`, with no additional policies specified at the subresource request.
And when filled with the following values from a selection:
- `delivery_type`: `"http-rp"`
- `delivery_key`: `"referrerPolicy"`
- `delivery_value`: `"no-referrer-when-downgrade"`
This becomes:
```json
"worker-classic": {
"sourceContextList": [
{
"sourceContextType": "top",
"policyDeliveries": [
{
"deliveryType": "meta",
"key": "referrerPolicy",
"value": "no-referrer"
}
]
},
{
"sourceContextType": "worker-classic",
"policyDeliveries": [
{
"deliveryType": "http-rp",
"key": "referrerPolicy",
"value": "no-referrer-when-downgrade"
}
]
}
],
"subresourcePolicyDeliveries": []
}
```
which means
- The top-level Document has `<meta name="referrer" content="no-referrer">`.
- The classic worker is created with
`Referrer-Policy: no-referrer-when-downgrade` HTTP response headers.
### Scenario Object
The **scenario** object is the JSON object written to the generated HTML files, and passed to JavaScript test runtime (as an argument of `TestCase`).
A scenario object is an selection object, minus the keys used in [SourceContext Resolution](#sourceContext-resolution):
- `source_context_list`
- `delivery_type`
- `delivery_key`
- `delivery_value`
plus the keys instantiated by [SourceContext Resolution](#sourceContext-resolution):
- `source_context_list`, except for the first `"top"` entry.
- `subresource_policy_deliveries`
For example:
```json
{
"source_context_list": [
{
"sourceContextType": "worker-classic",
"policyDeliveries": [
{
"deliveryType": "http-rp",
"key": "referrerPolicy",
"value": "no-referrer-when-downgrade"
}
]
}
],
"subresource_policy_deliveries": [],
"expectation": "stripped-referrer",
"origin": "same-http",
"redirection": "no-redirect",
"source_scheme": "http",
"subresource": "fetch"
}
```
### TopLevelPolicyDelivery Object
The ***TopLevelPolicyDelivery** object is the first `"top"` entry of `SourceContextList` instantiated by [SourceContext Resolution](#sourceContext-resolution), which represents the policy delivery of the top-level HTML Document.
The generator generates `<meta>` elements and `.headers` files of the top-level HTML files from the TopLevelPolicyDelivery object.
This is handled separately by the generator from other parts of selection objects and scenario objects, because the `<meta>` and `.headers` are hard-coded directly to the files in the WPT repository, while policies of subcontexts are generated via server-side `common/security-features/scope` scripts.
TODO(https://github.com/web-platform-tests/wpt/issues/21710): Currently the name `TopLevelPolicyDelivery` doesn't appear in the code.
## How the test runtime works
All the information needed at runtime is contained in an scenario object. See the code/comments of the following files.
- `project-directory/generic/test-case.js` defines `TestCase`, the entry point that receives a scenario object. `resources/common.sub.js` does the most of common JavaScript work.
- Subresource URLs (which point to `subresource/` scripts) are calculated from `origin` and `redirection` values.
- Initiating fetch requests based on `subresource` and `subresource_policy_deliveries`.
- `scope/` server-side scripts serve non-toplevel contexts, while the top-level Document is generated by the generator.
TODO(https://github.com/web-platform-tests/wpt/issues/21709): Merge the logics of `scope/` and the generator.
- `subresource/` server-side scripts serve subresource responses.

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1 @@
Access-Control-Allow-Origin: *

View File

@ -0,0 +1,30 @@
<!DOCTYPE html>
<html>
<head>
%(meta)s
<script src="/common/security-features/resources/common.sub.js"></script>
<script>
// Receive a message from the parent and start the test.
function onMessageFromParent(event) {
// Because this window might receive messages from child iframe during
// tests, we first remove the listener here before staring the test.
window.removeEventListener('message', onMessageFromParent);
const configurationError = "%(error)s";
if (configurationError.length > 0) {
parent.postMessage({error: configurationError}, "*");
return;
}
invokeRequest(event.data.subresource,
event.data.sourceContextList)
.then(result => parent.postMessage(result, "*"))
.catch(e => {
const message = (e.error && e.error.stack) || e.message || "Error";
parent.postMessage({error: message}, "*");
});
}
window.addEventListener('message', onMessageFromParent);
</script>
</head>
</html>

View File

@ -0,0 +1,29 @@
%(import)s
if ('DedicatedWorkerGlobalScope' in self &&
self instanceof DedicatedWorkerGlobalScope) {
self.onmessage = event => onMessageFromParent(event, self);
} else if ('SharedWorkerGlobalScope' in self &&
self instanceof SharedWorkerGlobalScope) {
onconnect = event => {
const port = event.ports[0];
port.onmessage = event => onMessageFromParent(event, port);
};
}
// Receive a message from the parent and start the test.
function onMessageFromParent(event, port) {
const configurationError = "%(error)s";
if (configurationError.length > 0) {
port.postMessage({error: configurationError});
return;
}
invokeRequest(event.data.subresource,
event.data.sourceContextList)
.then(result => port.postMessage(result))
.catch(e => {
const message = (e.error && e.error.stack) || e.message || "Error";
port.postMessage({error: message});
});
}

View File

@ -0,0 +1,16 @@
<!DOCTYPE html>
<html>
<head>
<title>This page reports back it's request details to the parent frame</title>
</head>
<body>
<script>
var result = {
location: document.location.toString(),
referrer: document.referrer.length > 0 ? document.referrer : undefined,
headers: %(headers)s
};
parent.postMessage(result, "*");
</script>
</body>
</html>

View File

@ -0,0 +1,9 @@
@font-face {
font-family: 'wpt';
font-style: normal;
font-weight: normal;
src: url(/common/security-features/subresource/font.py?id=%(id)s) format('truetype');
}
body {
font-family: 'wpt';
}

View File

@ -0,0 +1,3 @@
div.styled::before {
content:url(/common/security-features/subresource/image.py?id=%(id)s)
}

View File

@ -0,0 +1,3 @@
postMessage({
"headers": %(headers)s
}, "*");

View File

@ -0,0 +1,5 @@
onconnect = function(e) {
e.ports[0].postMessage({
"headers": %(headers)s
});
};

View File

@ -0,0 +1 @@
import '%(import_url)s';

View File

@ -0,0 +1,3 @@
path {
%(property)s: url(/common/security-features/subresource/svg.py?id=%(id)s#invalidFragment);
}

View File

@ -0,0 +1,5 @@
<?xml version='1.0' standalone='no'?>
<?xml-stylesheet href='stylesheet.py?id=%(id)s&amp;type=svg&amp;property=%(property)s' type='text/css'?>
<svg xmlns='http://www.w3.org/2000/svg'>
<path d='M 50,5 95,100 5,100 z' />
</svg>

After

Width:  |  Height:  |  Size: 228 B

View File

@ -0,0 +1,3 @@
postMessage({
"headers": %(headers)s
});

View File

@ -0,0 +1,533 @@
{
"selection_pattern": "%(source_context_list)s.%(delivery_type)s/%(delivery_value)s/%(subresource)s/%(origin)s.%(redirection)s.%(source_scheme)s",
"test_file_path_pattern": "gen/%(source_context_list)s.%(delivery_type)s/%(delivery_value)s/%(subresource)s.%(source_scheme)s.html",
"excluded_tests": [
{
// Workers are same-origin only
"expansion": "*",
"source_scheme": "*",
"source_context_list": "*",
"delivery_type": "*",
"delivery_value": "*",
"redirection": "*",
"subresource": [
"worker-classic",
"worker-module",
"sharedworker-classic",
"sharedworker-module"
],
"origin": [
"cross-https",
"cross-http",
"cross-http-downgrade",
"cross-wss",
"cross-ws",
"cross-ws-downgrade"
],
"expectation": "*"
},
{
// Workers are same-origin only (redirects)
"expansion": "*",
"source_scheme": "*",
"source_context_list": "*",
"delivery_type": "*",
"delivery_value": "*",
"redirection": [
"swap-origin",
"swap-scheme"
],
"subresource": [
"worker-classic",
"worker-module",
"sharedworker-classic",
"sharedworker-module"
],
"origin": "*",
"expectation": "*"
},
{
// Websockets are ws/wss-only
"expansion": "*",
"source_scheme": "*",
"source_context_list": "*",
"delivery_type": "*",
"delivery_value": "*",
"redirection": "*",
"subresource": "websocket",
"origin": [
"same-https",
"same-http",
"same-http-downgrade",
"cross-https",
"cross-http",
"cross-http-downgrade"
],
"expectation": "*"
},
{
// Redirects are intentionally forbidden in browsers:
// https://fetch.spec.whatwg.org/#concept-websocket-establish
// Websockets are no-redirect only
"expansion": "*",
"source_scheme": "*",
"source_context_list": "*",
"delivery_type": "*",
"delivery_value": "*",
"redirection": [
"keep-origin",
"swap-origin",
"keep-scheme",
"swap-scheme",
"downgrade"
],
"subresource": "websocket",
"origin": "*",
"expectation": "*"
},
{
// ws/wss are websocket-only
"expansion": "*",
"source_scheme": "*",
"source_context_list": "*",
"delivery_type": "*",
"delivery_value": "*",
"redirection": "*",
"subresource": [
"a-tag",
"area-tag",
"audio-tag",
"beacon",
"fetch",
"iframe-tag",
"img-tag",
"link-css-tag",
"link-prefetch-tag",
"object-tag",
"picture-tag",
"script-tag",
"script-tag-dynamic-import",
"sharedworker-classic",
"sharedworker-import",
"sharedworker-import-data",
"sharedworker-module",
"video-tag",
"worker-classic",
"worker-import",
"worker-import-data",
"worker-module",
"worklet-animation",
"worklet-animation-import-data",
"worklet-audio",
"worklet-audio-import-data",
"worklet-layout",
"worklet-layout-import-data",
"worklet-paint",
"worklet-paint-import-data",
"xhr"
],
"origin": [
"same-wss",
"same-ws",
"same-ws-downgrade",
"cross-wss",
"cross-ws",
"cross-ws-downgrade"
],
"expectation": "*"
},
{
// Worklets are HTTPS contexts only
"expansion": "*",
"source_scheme": "http",
"source_context_list": "*",
"delivery_type": "*",
"delivery_value": "*",
"redirection": "*",
"subresource": [
"worklet-animation",
"worklet-animation-import-data",
"worklet-audio",
"worklet-audio-import-data",
"worklet-layout",
"worklet-layout-import-data",
"worklet-paint",
"worklet-paint-import-data"
],
"origin": "*",
"expectation": "*"
}
],
"source_context_schema": {
"supported_subresource": {
"top": "*",
"iframe": "*",
"iframe-blank": "*",
"srcdoc": "*",
"worker-classic": [
"xhr",
"fetch",
"websocket",
"worker-classic",
"worker-module"
],
"worker-module": [
"xhr",
"fetch",
"websocket",
"worker-classic",
"worker-module"
],
"worker-classic-data": [
"xhr",
"fetch",
"websocket"
],
"worker-module-data": [
"xhr",
"fetch",
"websocket"
],
"sharedworker-classic": [
"xhr",
"fetch",
"websocket"
],
"sharedworker-module": [
"xhr",
"fetch",
"websocket"
],
"sharedworker-classic-data": [
"xhr",
"fetch",
"websocket"
],
"sharedworker-module-data": [
"xhr",
"fetch",
"websocket"
]
}
},
"source_context_list_schema": {
// Warning: Currently, some nested patterns of contexts have different
// inheritance rules for different kinds of policies.
// The generated tests will be used to test/investigate the policy
// inheritance rules, and eventually the policy inheritance rules will
// be unified (https://github.com/w3ctag/design-principles/issues/111).
"top": {
"description": "Policy set by the top-level Document",
"sourceContextList": [
{
"sourceContextType": "top",
"policyDeliveries": [
"policy"
]
}
],
"subresourcePolicyDeliveries": []
},
"req": {
"description": "Subresource request's policy should override Document's policy",
"sourceContextList": [
{
"sourceContextType": "top",
"policyDeliveries": [
"anotherPolicy"
]
}
],
"subresourcePolicyDeliveries": [
"nonNullPolicy"
]
},
"srcdoc-inherit": {
"description": "srcdoc iframe without its own policy should inherit parent Document's policy",
"sourceContextList": [
{
"sourceContextType": "top",
"policyDeliveries": [
"policy"
]
},
{
"sourceContextType": "srcdoc"
}
],
"subresourcePolicyDeliveries": []
},
"srcdoc": {
"description": "srcdoc iframe's policy should override parent Document's policy",
"sourceContextList": [
{
"sourceContextType": "top",
"policyDeliveries": [
"anotherPolicy"
]
},
{
"sourceContextType": "srcdoc",
"policyDeliveries": [
"nonNullPolicy"
]
}
],
"subresourcePolicyDeliveries": []
},
"iframe": {
"description": "external iframe's policy should override parent Document's policy",
"sourceContextList": [
{
"sourceContextType": "top",
"policyDeliveries": [
"anotherPolicy"
]
},
{
"sourceContextType": "iframe",
"policyDeliveries": [
"policy"
]
}
],
"subresourcePolicyDeliveries": []
},
"iframe-blank-inherit": {
"description": "blank iframe should inherit parent Document's policy",
"sourceContextList": [
{
"sourceContextType": "top",
"policyDeliveries": [
"policy"
]
},
{
"sourceContextType": "iframe-blank"
}
],
"subresourcePolicyDeliveries": []
},
"worker-classic": {
// This is applicable to referrer-policy tests.
// Use "worker-classic-inherit" for CSP (mixed-content, etc.).
"description": "dedicated workers shouldn't inherit its parent's policy.",
"sourceContextList": [
{
"sourceContextType": "top",
"policyDeliveries": [
"anotherPolicy"
]
},
{
"sourceContextType": "worker-classic",
"policyDeliveries": [
"policy"
]
}
],
"subresourcePolicyDeliveries": []
},
"worker-classic-data": {
"description": "data: dedicated workers should inherit its parent's policy.",
"sourceContextList": [
{
"sourceContextType": "top",
"policyDeliveries": [
"policy"
]
},
{
"sourceContextType": "worker-classic-data",
"policyDeliveries": []
}
],
"subresourcePolicyDeliveries": []
},
"worker-module": {
// This is applicable to referrer-policy tests.
"description": "dedicated workers shouldn't inherit its parent's policy.",
"sourceContextList": [
{
"sourceContextType": "top",
"policyDeliveries": [
"anotherPolicy"
]
},
{
"sourceContextType": "worker-module",
"policyDeliveries": [
"policy"
]
}
],
"subresourcePolicyDeliveries": []
},
"worker-module-data": {
"description": "data: dedicated workers should inherit its parent's policy.",
"sourceContextList": [
{
"sourceContextType": "top",
"policyDeliveries": [
"policy"
]
},
{
"sourceContextType": "worker-module-data",
"policyDeliveries": []
}
],
"subresourcePolicyDeliveries": []
},
"sharedworker-classic": {
"description": "shared workers shouldn't inherit its parent's policy.",
"sourceContextList": [
{
"sourceContextType": "top",
"policyDeliveries": [
"anotherPolicy"
]
},
{
"sourceContextType": "sharedworker-classic",
"policyDeliveries": [
"policy"
]
}
],
"subresourcePolicyDeliveries": []
},
"sharedworker-classic-data": {
"description": "data: shared workers should inherit its parent's policy.",
"sourceContextList": [
{
"sourceContextType": "top",
"policyDeliveries": [
"policy"
]
},
{
"sourceContextType": "sharedworker-classic-data",
"policyDeliveries": []
}
],
"subresourcePolicyDeliveries": []
},
"sharedworker-module": {
"description": "shared workers shouldn't inherit its parent's policy.",
"sourceContextList": [
{
"sourceContextType": "top",
"policyDeliveries": [
"anotherPolicy"
]
},
{
"sourceContextType": "sharedworker-module",
"policyDeliveries": [
"policy"
]
}
],
"subresourcePolicyDeliveries": []
},
"sharedworker-module-data": {
"description": "data: shared workers should inherit its parent's policy.",
"sourceContextList": [
{
"sourceContextType": "top",
"policyDeliveries": [
"policy"
]
},
{
"sourceContextType": "sharedworker-module-data",
"policyDeliveries": []
}
],
"subresourcePolicyDeliveries": []
}
},
"test_expansion_schema": {
"expansion": [
"default",
"override"
],
"source_scheme": [
"http",
"https"
],
"source_context_list": [
"top",
"req",
"srcdoc-inherit",
"srcdoc",
"iframe",
"iframe-blank-inherit",
"worker-classic",
"worker-classic-data",
"worker-module",
"worker-module-data",
"sharedworker-classic",
"sharedworker-classic-data",
"sharedworker-module",
"sharedworker-module-data"
],
"redirection": [
"no-redirect",
"keep-origin",
"swap-origin",
"keep-scheme",
"swap-scheme",
"downgrade"
],
"origin": [
"same-https",
"same-http",
"same-http-downgrade",
"cross-https",
"cross-http",
"cross-http-downgrade",
"same-wss",
"same-ws",
"same-ws-downgrade",
"cross-wss",
"cross-ws",
"cross-ws-downgrade"
],
"subresource": [
"a-tag",
"area-tag",
"audio-tag",
"beacon",
"fetch",
"iframe-tag",
"img-tag",
"link-css-tag",
"link-prefetch-tag",
"object-tag",
"picture-tag",
"script-tag",
"script-tag-dynamic-import",
"sharedworker-classic",
"sharedworker-import",
"sharedworker-import-data",
"sharedworker-module",
"video-tag",
"websocket",
"worker-classic",
"worker-import",
"worker-import-data",
"worker-module",
"worklet-animation",
"worklet-animation-import-data",
"worklet-audio",
"worklet-audio-import-data",
"worklet-layout",
"worklet-layout-import-data",
"worklet-paint",
"worklet-paint-import-data",
"xhr"
]
}
}

View File

@ -0,0 +1 @@
<!-- DO NOT EDIT! Generated by `%(generating_script_filename)s --spec %(spec_directory)s/` -->

View File

@ -0,0 +1 @@
var SPEC_JSON = %(spec_json)s;

View File

@ -0,0 +1,26 @@
<!DOCTYPE html>
%(generated_disclaimer)s
<html>
<head>
<meta charset="utf-8">
<meta name="timeout" content="long">%(meta_delivery_method)s
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/common/security-features/resources/common.sub.js"></script>
<!-- The original specification JSON for validating the scenario. -->
<script src="%(spec_json_js)s"></script>
<!-- Internal checking of the tests -->
<script src="%(sanity_checker_js)s"></script>
%(helper_js)s </head>
<body>
<script>
TestCase(
[
%(scenarios)s
],
new SanityChecker()
).start();
</script>
<div id="log"></div>
</body>
</html>

View File

@ -0,0 +1,22 @@
<!DOCTYPE html>
%(generated_disclaimer)s
<html>
<head>
<meta charset="utf-8">
<meta name="timeout" content="long">%(meta_delivery_method)s
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/common/security-features/resources/common.sub.js"></script>
%(helper_js)s </head>
<body>
<script>
TestCase(
[
%(scenarios)s
],
new SanityChecker()
).start();
</script>
<div id="log"></div>
</body>
</html>

View File

@ -0,0 +1,62 @@
# Types around the generator and generated tests
This document describes types and concepts used across JavaScript and Python parts of this test framework.
Please refer to the JSDoc in `common.sub.js` or docstrings in Python scripts (if any).
## Scenario
### Properties
- All keys of `test_expansion_schema` in `spec.src.json`, except for `expansion`, `delivery_type`, `delivery_value`, and `source_context_list`. Their values are **string**s specified in `test_expansion_schema`.
- `source_context_list`
- `subresource_policy_deliveries`
### Types
- Generator (`spec.src.json`): JSON object
- Generator (Python): `dict`
- Runtime (JS): JSON object
- Runtime (Python): N/A
## `PolicyDelivery`
### Types
- Generator (`spec.src.json`): JSON object
- Generator (Python): `util.PolicyDelivery`
- Runtime (JS): JSON object (`@typedef PolicyDelivery` in `common.sub.js`)
- Runtime (Python): N/A
## `SourceContext`
Subresource requests can be possibly sent from various kinds of fetch client's environment settings objects. For example:
- top-level windows,
- `<iframe>`s, or
- `WorkerGlobalScope`s.
A **`SourceContext`** object specifies one environment settings object, and an Array of `SourceContext` specifies a possibly nested context, from the outer-most to inner-most environment settings objects.
Note: The top-level document is processed and trimmed by the generator, and is not included in the `sourceContextList` field of `Scenario` in the generated output.
For example, `[{sourceContextType: "srcdoc"}, {sourceContextType: "worker-classic"}]` means that a subresource request is to be sent from a classic dedicated worker created from `<iframe srcdoc>` inside the top-level HTML Document.
Note: A `SourceContext` (or an array of `SourceContext`) is set based on the fetch client's settings object that is used for the subresource fetch, NOT on the module map settings object nor on the inner-most settings object that appears in the test.
For example, the `sourceContextList` field of `Scenario` is `[]` (indicating the top-level Window):
- When testing top-level worker script fetch, e.g. `new Worker('worker.js')`. There is `WorkerGlobalScope` created from `worker.js`, but it isn't the fetch client's settings object used for fetching `worker.js` itself.
- When testing worker script imported from the root worker script, e.g. `new Worker('top.js', {type: 'module'})` where `top.js` has `import 'worker.js'`. Again, the fetch client's settings object used for `worker.js` is the top-level Window, not `WorkerGlobalScope` created by `top.js`.
### Properties
- `sourceContextType`: A string specifying the kind of the source context to be used.
Valid values are the keys of `sourceContextMap` in `common.sub.js`, or `"top"` indicating the top-level Document (`"top"` is valid/used only in the generator).
- `policyDeliveries`: A list of `PolicyDelivery` applied to the source context.
### Types
- Generator (`spec.src.json`): JSON object
- Generator (Python): `util.SourceContext`
- Runtime (JS): JSON object (`@typedef SourceContext` in `common.sub.js`)
- Runtime (Python): N/A

BIN
test/fixtures/wpt/common/square.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -0,0 +1,57 @@
/**
* Runs tests for <https://webidl.spec.whatwg.org/#es-stringifier>.
* @param {Object} aObject - object to test
* @param {string} aAttribute - IDL attribute name that is annotated with `stringifier`
* @param {boolean} aIsUnforgeable - whether the IDL attribute is `[LegacyUnforgeable]`
*/
function test_stringifier_attribute(aObject, aAttribute, aIsUnforgeable) {
// Step 1.
test(function() {
[null, undefined].forEach(function(v) {
assert_throws_js(TypeError, function() {
aObject.toString.call(v);
});
});
});
// TODO Step 2: security check.
// Step 3.
test(function() {
assert_false("Window" in window && aObject instanceof window.Window);
[{}, window].forEach(function(v) {
assert_throws_js(TypeError, function() {
aObject.toString.call(v)
});
});
});
// Step 4-6.
var expected_value;
test(function() {
expected_value = aObject[aAttribute];
assert_equals(aObject[aAttribute], expected_value,
"The attribute " + aAttribute + " should be pure.");
});
var test_error = { name: "test" };
test(function() {
if (!aIsUnforgeable) {
Object.defineProperty(aObject, aAttribute, {
configurable: true,
get: function() { throw test_error; }
});
}
assert_equals(aObject.toString(), expected_value);
});
test(function() {
if (!aIsUnforgeable) {
Object.defineProperty(aObject, aAttribute, {
configurable: true,
value: { toString: function() { throw test_error; } }
});
}
assert_equals(aObject.toString(), expected_value);
});
}

View File

@ -0,0 +1 @@
Content-Type: text/javascript; charset=utf-8

View File

@ -0,0 +1,83 @@
(function() {
var subTestKeyPattern = null;
var match;
var collectKeys = false;
var collectCounts = false;
var keys = {};
var exclude = false;
if (location.search) {
match = /(?:^\?|&)(include|exclude)=([^&]+)?/.exec(location.search);
if (match) {
subTestKeyPattern = new RegExp(`^${match[2]}$`);
if (match[1] === 'exclude') {
exclude = true;
}
}
// Below is utility code to generate <meta> for copy/paste into tests.
// Sample usage:
// test.html?get-keys
match = /(?:^\?|&)get-keys(&get-counts)?(?:&|$)/.exec(location.search);
if (match) {
collectKeys = true;
if (match[1]) {
collectCounts = true;
}
add_completion_callback(() => {
var metas = [];
var template = '<meta name="variant" content="?include=%s">';
if (collectCounts) {
template += ' <!--%s-->';
}
for (var key in keys) {
var meta = template.replace("%s", key);
if (collectCounts) {
meta = meta.replace("%s", keys[key]);
}
metas.push(meta);
}
var pre = document.createElement('pre');
pre.textContent = metas.join('\n') + '\n';
document.body.insertBefore(pre, document.body.firstChild);
document.getSelection().selectAllChildren(pre);
});
}
}
/**
* Check if `key` is in the subset specified in the URL.
* @param {string} key
* @returns {boolean}
*/
function shouldRunSubTest(key) {
if (key && subTestKeyPattern) {
var found = subTestKeyPattern.test(key);
if (exclude) {
return !found;
}
return found;
}
return true;
}
/**
* Only test a subset of tests with `?include=Foo` or `?exclude=Foo` in the URL.
* Can be used together with `<meta name="variant" content="...">`
* Sample usage:
* for (const test of tests) {
* subsetTestByKey("Foo", async_test, test.fn, test.name);
* }
*/
function subsetTestByKey(key, testFunc, ...args) {
if (collectKeys) {
if (collectCounts && key in keys) {
keys[key]++;
} else {
keys[key] = 1;
}
}
if (shouldRunSubTest(key)) {
return testFunc(...args);
}
return null;
}
self.shouldRunSubTest = shouldRunSubTest;
self.subsetTestByKey = subsetTestByKey;
})();

View File

@ -0,0 +1,60 @@
(function() {
var subTestStart = 0;
var subTestEnd = Infinity;
var match;
if (location.search) {
match = /(?:^\?|&)(\d+)-(\d+|last)(?:&|$)/.exec(location.search);
if (match) {
subTestStart = parseInt(match[1], 10);
if (match[2] !== "last") {
subTestEnd = parseInt(match[2], 10);
}
}
// Below is utility code to generate <meta> for copy/paste into tests.
// Sample usage:
// test.html?split=1000
match = /(?:^\?|&)split=(\d+)(?:&|$)/.exec(location.search);
if (match) {
var testsPerVariant = parseInt(match[1], 10);
add_completion_callback(tests => {
var total = tests.length;
var template = '<meta name="variant" content="?%s-%s">';
var metas = [];
for (var i = 1; i < total - testsPerVariant; i = i + testsPerVariant) {
metas.push(template.replace("%s", i).replace("%s", i + testsPerVariant - 1));
}
metas.push(template.replace("%s", i).replace("%s", "last"));
var pre = document.createElement('pre');
pre.textContent = metas.join('\n');
document.body.insertBefore(pre, document.body.firstChild);
document.getSelection().selectAllChildren(pre);
});
}
}
/**
* Check if `currentSubTest` is in the subset specified in the URL.
* @param {number} currentSubTest
* @returns {boolean}
*/
function shouldRunSubTest(currentSubTest) {
return currentSubTest >= subTestStart && currentSubTest <= subTestEnd;
}
var currentSubTest = 0;
/**
* Only test a subset of tests with, e.g., `?1-10` in the URL.
* Can be used together with `<meta name="variant" content="...">`
* Sample usage:
* for (const test of tests) {
* subsetTest(async_test, test.fn, test.name);
* }
*/
function subsetTest(testFunc, ...args) {
currentSubTest++;
if (shouldRunSubTest(currentSubTest)) {
return testFunc(...args);
}
return null;
}
self.shouldRunSubTest = shouldRunSubTest;
self.subsetTest = subsetTest;
})();

View File

@ -0,0 +1,67 @@
self.testSettingImmutablePrototypeToNewValueOnly =
(prefix, target, newValue, newValueString, { isSameOriginDomain },
targetGlobal = window) => {
test(() => {
assert_throws_js(TypeError, () => {
Object.setPrototypeOf(target, newValue);
});
}, `${prefix}: setting the prototype to ${newValueString} via Object.setPrototypeOf should throw a TypeError`);
let dunderProtoError = "SecurityError";
let dunderProtoErrorName = "\"SecurityError\" DOMException";
if (isSameOriginDomain) {
// We're going to end up calling the __proto__ setter, which will
// enter the Realm of targetGlobal before throwing.
dunderProtoError = targetGlobal.TypeError;
dunderProtoErrorName = "TypeError";
}
test(() => {
const func = function() {
target.__proto__ = newValue;
};
if (isSameOriginDomain) {
assert_throws_js(dunderProtoError, func);
} else {
assert_throws_dom(dunderProtoError, func);
}
}, `${prefix}: setting the prototype to ${newValueString} via __proto__ should throw a ${dunderProtoErrorName}`);
test(() => {
assert_false(Reflect.setPrototypeOf(target, newValue));
}, `${prefix}: setting the prototype to ${newValueString} via Reflect.setPrototypeOf should return false`);
};
self.testSettingImmutablePrototype =
(prefix, target, originalValue, { isSameOriginDomain }, targetGlobal = window) => {
const newValue = {};
const newValueString = "an empty object";
testSettingImmutablePrototypeToNewValueOnly(prefix, target, newValue, newValueString, { isSameOriginDomain }, targetGlobal);
const originalValueString = originalValue === null ? "null" : "its original value";
test(() => {
assert_equals(Object.getPrototypeOf(target), originalValue);
}, `${prefix}: the prototype must still be ${originalValueString}`);
test(() => {
Object.setPrototypeOf(target, originalValue);
}, `${prefix}: setting the prototype to ${originalValueString} via Object.setPrototypeOf should not throw`);
if (isSameOriginDomain) {
test(() => {
target.__proto__ = originalValue;
}, `${prefix}: setting the prototype to ${originalValueString} via __proto__ should not throw`);
} else {
test(() => {
assert_throws_dom("SecurityError", function() {
target.__proto__ = newValue;
});
}, `${prefix}: setting the prototype to ${originalValueString} via __proto__ should throw a "SecurityError" since ` +
`it ends up in CrossOriginGetOwnProperty`);
}
test(() => {
assert_true(Reflect.setPrototypeOf(target, originalValue));
}, `${prefix}: setting the prototype to ${originalValueString} via Reflect.setPrototypeOf should return true`);
};

View File

@ -0,0 +1 @@
Content-Type: text/javascript; charset=utf-8

View File

@ -0,0 +1,4 @@
This is a sample text/plain document.
This is not an HTML document.

View File

@ -0,0 +1,934 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- -*- Mode: HTML; tab-width: 2; indent-tabs-mode: nil; -*- -->
<!-- vim: set shiftwidth=2 tabstop=2 autoindent expandtab: -->
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<!--
Features to add:
* make the left and right parts of the viewer independently scrollable
* make the test list filterable
** default to only showing unexpecteds
* add other ways to highlight differences other than circling?
* add zoom/pan to images
* Add ability to load log via XMLHttpRequest (also triggered via URL param)
* color the test list based on pass/fail and expected/unexpected/random/skip
* ability to load multiple logs ?
** rename them by clicking on the name and editing
** turn the test list into a collapsing tree view
** move log loading into popup from viewer UI
-->
<!DOCTYPE html>
<html lang="en-US" xml:lang="en-US" xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Reftest analyzer</title>
<style type="text/css"><![CDATA[
html, body { margin: 0; }
html { padding: 0; }
body { padding: 4px; }
#pixelarea, #itemlist, #images { position: absolute; }
#itemlist, #images { overflow: auto; }
#pixelarea { top: 0; left: 0; width: 320px; height: 84px; overflow: visible }
#itemlist { top: 84px; left: 0; width: 320px; bottom: 0; }
#images { top: 0; bottom: 0; left: 320px; right: 0; }
#leftpane { width: 320px; }
#images { position: fixed; top: 10px; left: 340px; }
form#imgcontrols { margin: 0; display: block; }
#itemlist > table { border-collapse: collapse; }
#itemlist > table > tbody > tr > td { border: 1px solid; padding: 1px; }
#itemlist td.activeitem { background-color: yellow; }
/*
#itemlist > table > tbody > tr.pass > td.url { background: lime; }
#itemlist > table > tbody > tr.fail > td.url { background: red; }
*/
#magnification > svg { display: block; width: 84px; height: 84px; }
#pixelinfo { font: small sans-serif; position: absolute; width: 200px; left: 84px; }
#pixelinfo table { border-collapse: collapse; }
#pixelinfo table th { white-space: nowrap; text-align: left; padding: 0; }
#pixelinfo table td { font-family: monospace; padding: 0 0 0 0.25em; }
#pixelhint { display: inline; color: #88f; cursor: help; }
#pixelhint > * { display: none; position: absolute; margin: 8px 0 0 8px; padding: 4px; width: 400px; background: #ffa; color: black; box-shadow: 3px 3px 2px #888; z-index: 1; }
#pixelhint:hover { color: #000; }
#pixelhint:hover > * { display: block; }
#pixelhint p { margin: 0; }
#pixelhint p + p { margin-top: 1em; }
]]></style>
<script type="text/javascript"><![CDATA[
var XLINK_NS = "http://www.w3.org/1999/xlink";
var SVG_NS = "http://www.w3.org/2000/svg";
var IMAGE_NOT_AVAILABLE = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAKAAAAASCAYAAADczdVTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAHy0lEQVRoge2aX2hb5xnGf2dYabROgqQkpMuKnWUJLmxHMFaa/SscteQiF5EvUgqLctEVrDJKK1+MolzkQr4IctgW+SLIheJc1BpFpswJw92FbaZsTCGTL0465AtntUekJdJ8lByVHbnnwLsLKbKdSJbiZBVjeuAYn+/P+z3fc97vfd9zbEVEhB566BK+1m0CPfx/o+eAPXQVbR3QqVapOl8FlR46h0O1Wu02iacCZfsasMKEz8vbx1JYE6fY/dXx6mEbFObPcvDVDBlznpc9G+2r8xNcvLqK2w39r4UI+fs7tFjmytgFFu718865EIebPGincI3zFz7Bcrtx97/GL0P+p+IPbSOgRwXtW3vpewqL/a/g5rgf39hit2m0hGUAHOHrrq3trmef4/lDB7Ay57n01zuPZXPX7jUunv+Yf9ktR7D/0CHca7/n3KXPsHbAuynkCWCZptgiImKLaVqP9NuW1bT9ceybpr3j+WJbYrVa3rbEatGZi2uixvWdrysilmWKae2M+5PqlktoosayLfubcrN10dAk24aynUsIxMVsadwUs+EX7dEyAlaXLqMoCj6fj5HkUqO9MD+Govjx+xXcXi+uoRAhvwuv182Z8Ws4AJUlxoZ8uNxuvF43ii/EtdXNNUuV68lR/IqC4gsxPj7KkE/BF5qmClRXrzFSt+/1ulDOjLNU6eQ4OcyPDqH4hhg5O4LicuN2K4xcvk6jjHUKJM8O1fvcKMoZkouFOq1VPp1OcuXGAvrvfsv0lWmSySTzN0sdH+jyYhK/ouB2e/G6XfjPJikBVG8SUhT8fl99nwVGfQp+vx+f4iO5VO1AtwJjfgXF58M/kqSVJP9ef0xuAI6NlwWmL41xxqeg+PyMXr72yBqW3cI4JaZHh1DcXrxeLy5liORiB7q1PiZFyeV0mQqz9TRZeUmFVUGLSjqdkgCIFp2RTCosEJOiiIihSyKWkDl9WYrFnCQCCNF0w0QmHhBQJTEzJ+nZSQmAoEYks2KIGBkJgASiM5I3LbGMnCSCCEQl38GJMvMZiag1e+nlFcmmIgKaZEwREaPGhWGZ1VfEMFZkNj4sgCSyhoihSzwSlqCGoAUlEo1IJByW+Oxyh+dZJJ+eklhiRnIrRcnrM6KCxLOmiNiipyICSGR2pTY2O1m7T2XEsNrrJmJLfjkn6amwoMbFaMEhG28eAVtzExErW3sOBCWVzkpmNiEqCOEZ2RyLTT3eJAKaMhVEUMOSXjHEtg3JTIUFkNTK9rGwbQrWm2xGb6QoWxIqEtdtEWO28aDtoi6JSFCAjUtL1AUzJA4SSW/IZ2VjjU0V0zEBJBiJSzwWk1g8IZEAAmrdidrBkoSKxB4IW08tGVNEzIxoIJM5a8v4SQ1RY5lGSy6x8xScz6QkHFBre1Zre49nH+y1KDEQLV7TcyU1LBCtHVppp9smxk2dYAMtHXA7blZWNJDZ4sZ4MxPbdHjrbc3WNuvOq4YlkYhLLBaXeKx2sLcrBUS2ScFtUbUBh3WgajvgOYgGuKjw4Rsqb1uvkssbWLbJXFQFqL/I9IEKa2WzYcqy16E2BNteB1R+cuwoRwcHGRx4nlfenWMuPclRDx3goSraqd+7Gj/Y5d76SrXLu3VKLYW1rMZbo/QpB4+9zt6fT1I0Law/LRMBaLzC7ePNuSgL7/2GpcotLr7+AZG5t9gH0Fa3zuFq1tiWG4DKs5tebV1NDDW1XYd26iWO9A8wODjAUfUN5ubm+Ch4ZFuuLRzQoVwqUCqXyN9fg3tFSuUShVIZhyr5O2vo94o42DwD/PP23fq8Bf5urLO+BoHBwxzc20c++wcmz+lAkWLFATwcf3+YDwIDhMYmuDw+wt5j5+C5ZwDYP/gSoLP6xX5+fOIkJ47/lIP8g49/Nc3tDj59OZUiRR3uFYsAVO/eZoE1yvkyeA6gAaff+zU3SxUcp8LilQucnoFTP3hhix19/garlQqFW9eZOBti9Mqt9mubXwBw+NALeDC4cfVDzgP3i3keUN/nf4uo+hEver/DRaK84/9mY/72uoFTKVMolVn5/HPgPvlSmVKhRL2bSrlEqVyidH8N/d7t2u/lakfcKneLgM4rvxhncbXA6tI8kTffB+0NjnrAqZYplcrk83ceXdtzgB+psHD7S/pfPs7JkydQB1x8dnWS2SVje9GaxkVLl+DmNNC4NJn/S6JxH5nJyNRwrW7Qi7oMgxBMyd9molvmRKO1cExgshG6l9NTEhkOynAkLlOJoKBuhPV8ZlK0h9aNTqVbv3ltEK/VIiAQEN0yZVLbuM+aImLoEgts3VdsJrfFil1M1/ZSv9RAROaWO8n/hkyF1Q3bgeFGygvPrDRG5Wcf1IJbq9rlNrrNbra96aqlUVMSWrNnNiw5uw23T/4o4Xq7FtA29h2My3K9WtETgRZr13UxdIk+pGswkpCcsX0N2OZD9BOgWqFsgWePp20KWb0ywkDgEIa8y55Gq0O5XKHP7cGz++l/haxWylgOuD17aG7eoVpxwL27RX8b27jZ42n1qdahXKrg2bfnUW0eQ7edoD232l+/LPp2pHvNfh8eT2f8/3sO2AZLyRAvns6gqToLOgxP6Uz87HvdoNJDF9E1B6ysLrLw5yW+3PUNvv3dH/L9wX3doNFDl9E1B+yhB+j9O1YPXcZ/AAl9BWJNvZE7AAAAAElFTkSuQmCC";
var gPhases = null;
var gIDCache = {};
var gMagPixPaths = []; // 2D array of array-of-two <path> objects used in the pixel magnifier
var gMagWidth = 5; // number of zoomed in pixels to show horizontally
var gMagHeight = 5; // number of zoomed in pixels to show vertically
var gMagZoom = 16; // size of the zoomed in pixels
var gImage1Data; // ImageData object for the reference image
var gImage2Data; // ImageData object for the test output image
var gFlashingPixels = []; // array of <path> objects that should be flashed due to pixel color mismatch
var gParams;
function ID(id) {
if (!(id in gIDCache))
gIDCache[id] = document.getElementById(id);
return gIDCache[id];
}
function hash_parameters() {
var result = { };
var params = window.location.hash.substr(1).split(/[&;]/);
for (var i = 0; i < params.length; i++) {
var parts = params[i].split("=");
result[parts[0]] = unescape(unescape(parts[1]));
}
return result;
}
function load() {
gPhases = [ ID("entry"), ID("loading"), ID("viewer") ];
build_mag();
gParams = hash_parameters();
if (gParams.log) {
show_phase("loading");
process_log(gParams.log);
} else if (gParams.logurl) {
show_phase("loading");
var req = new XMLHttpRequest();
req.onreadystatechange = function() {
if (req.readyState === 4) {
process_log(req.responseText);
}
};
req.open('GET', gParams.logurl, true);
req.send();
}
window.addEventListener('keypress', handle_keyboard_shortcut);
window.addEventListener('keydown', handle_keydown);
ID("image1").addEventListener('error', image_load_error);
ID("image2").addEventListener('error', image_load_error);
}
function image_load_error(e) {
e.target.setAttributeNS(XLINK_NS, "xlink:href", IMAGE_NOT_AVAILABLE);
}
function build_mag() {
var mag = ID("mag");
var r = document.createElementNS(SVG_NS, "rect");
r.setAttribute("x", gMagZoom * -gMagWidth / 2);
r.setAttribute("y", gMagZoom * -gMagHeight / 2);
r.setAttribute("width", gMagZoom * gMagWidth);
r.setAttribute("height", gMagZoom * gMagHeight);
mag.appendChild(r);
mag.setAttribute("transform", "translate(" + (gMagZoom * (gMagWidth / 2) + 1) + "," + (gMagZoom * (gMagHeight / 2) + 1) + ")");
for (var x = 0; x < gMagWidth; x++) {
gMagPixPaths[x] = [];
for (var y = 0; y < gMagHeight; y++) {
var p1 = document.createElementNS(SVG_NS, "path");
p1.setAttribute("d", "M" + ((x - gMagWidth / 2) + 1) * gMagZoom + "," + (y - gMagHeight / 2) * gMagZoom + "h" + -gMagZoom + "v" + gMagZoom);
p1.setAttribute("stroke", "black");
p1.setAttribute("stroke-width", "1px");
p1.setAttribute("fill", "#aaa");
var p2 = document.createElementNS(SVG_NS, "path");
p2.setAttribute("d", "M" + ((x - gMagWidth / 2) + 1) * gMagZoom + "," + (y - gMagHeight / 2) * gMagZoom + "v" + gMagZoom + "h" + -gMagZoom);
p2.setAttribute("stroke", "black");
p2.setAttribute("stroke-width", "1px");
p2.setAttribute("fill", "#888");
mag.appendChild(p1);
mag.appendChild(p2);
gMagPixPaths[x][y] = [p1, p2];
}
}
var flashedOn = false;
setInterval(function() {
flashedOn = !flashedOn;
flash_pixels(flashedOn);
}, 500);
}
function show_phase(phaseid) {
for (var i in gPhases) {
var phase = gPhases[i];
phase.style.display = (phase.id == phaseid) ? "" : "none";
}
if (phase == "viewer")
ID("images").style.display = "none";
}
function fileentry_changed() {
show_phase("loading");
var input = ID("fileentry");
var files = input.files;
if (files.length > 0) {
// Only handle the first file; don't handle multiple selection.
// The parts of the log we care about are ASCII-only. Since we
// can ignore lines we don't care about, best to read in as
// iso-8859-1, which guarantees we don't get decoding errors.
var fileReader = new FileReader();
fileReader.onload = function(e) {
var log = null;
log = e.target.result;
if (log)
process_log(log);
else
show_phase("entry");
}
fileReader.readAsText(files[0], "iso-8859-1");
}
// So the user can process the same filename again (after
// overwriting the log), clear the value on the form input so we
// will always get an onchange event.
input.value = "";
}
function log_pasted() {
show_phase("loading");
var entry = ID("logentry");
var log = entry.value;
entry.value = "";
process_log(log);
}
var gTestItems;
// This function is not used in production code, but can be invoked manually
// from the devtools console in order to test changes to the parsing regexes
// in process_log.
function test_parsing() {
// Note that the logs in these testcases have been manually edited to strip
// out stuff for brevity.
var testcases = [
{ "name": "empty log",
"log": "",
"expected": { "pass": 0, "unexpected": 0, "random": 0, "skip": 0 },
"expected_images": 0,
},
{ "name": "android log",
"log": `[task 2018-12-28T10:36:45.718Z] 10:36:45 INFO - REFTEST TEST-START | a == b
[task 2018-12-28T10:36:45.719Z] 10:36:45 INFO - REFTEST TEST-LOAD | a | 78 / 275 (28%)
[task 2018-12-28T10:36:56.138Z] 10:36:56 INFO - REFTEST TEST-LOAD | b | 78 / 275 (28%)
[task 2018-12-28T10:37:06.559Z] 10:37:06 INFO - REFTEST TEST-UNEXPECTED-FAIL | a == b | image comparison, max difference: 255, number of differing pixels: 5950
[task 2018-12-28T10:37:06.568Z] 10:37:06 INFO - REFTEST IMAGE 1 (TEST): data:image/png;base64,
[task 2018-12-28T10:37:06.577Z] 10:37:06 INFO - REFTEST IMAGE 2 (REFERENCE): data:image/png;base64,
[task 2018-12-28T10:37:06.577Z] 10:37:06 INFO - REFTEST INFO | Saved log: stuff trimmed here
[task 2018-12-28T10:37:06.582Z] 10:37:06 INFO - REFTEST TEST-END | a == b
[task 2018-12-28T10:37:06.583Z] 10:37:06 INFO - REFTEST TEST-START | a2 == b2
[task 2018-12-28T10:37:06.583Z] 10:37:06 INFO - REFTEST TEST-LOAD | a2 | 79 / 275 (28%)
[task 2018-12-28T10:37:06.584Z] 10:37:06 INFO - REFTEST TEST-LOAD | b2 | 79 / 275 (28%)
[task 2018-12-28T10:37:16.982Z] 10:37:16 INFO - REFTEST TEST-PASS | a2 == b2 | image comparison, max difference: 0, number of differing pixels: 0
[task 2018-12-28T10:37:16.982Z] 10:37:16 INFO - REFTEST TEST-END | a2 == b2`,
"expected": { "pass": 1, "unexpected": 1, "random": 0, "skip": 0 },
"expected_images": 2,
},
{ "name": "local reftest run (Linux)",
"log": `REFTEST TEST-START | file:///a == file:///b
REFTEST TEST-LOAD | file:///a | 73 / 86 (84%)
REFTEST TEST-LOAD | file:///b | 73 / 86 (84%)
REFTEST TEST-PASS | file:///a == file:///b | image comparison, max difference: 0, number of differing pixels: 0
REFTEST TEST-END | file:///a == file:///b`,
"expected": { "pass": 1, "unexpected": 0, "random": 0, "skip": 0 },
"expected_images": 0,
},
{ "name": "wpt reftests (Linux automation)",
"log": `16:50:43 INFO - TEST-START | /a
16:50:43 INFO - PID 4276 | 1548694243694 Marionette INFO Testing http://web-platform.test:8000/a == http://web-platform.test:8000/b
16:50:43 INFO - PID 4276 | 1548694243963 Marionette INFO No differences allowed
16:50:44 INFO - TEST-PASS | /a | took 370ms
16:50:44 INFO - TEST-START | /a2
16:50:44 INFO - PID 4276 | 1548694244066 Marionette INFO Testing http://web-platform.test:8000/a2 == http://web-platform.test:8000/b2
16:50:44 INFO - PID 4276 | 1548694244792 Marionette INFO No differences allowed
16:50:44 INFO - PID 4276 | 1548694244792 Marionette INFO Found 28 pixels different, maximum difference per channel 14
16:50:44 INFO - TEST-UNEXPECTED-FAIL | /a2 | Testing http://web-platform.test:8000/a2 == http://web-platform.test:8000/b2
16:50:44 INFO - REFTEST IMAGE 1 (TEST): data:image/png;base64,
16:50:44 INFO - REFTEST IMAGE 2 (REFERENCE): data:image/png;base64,
16:50:44 INFO - TEST-INFO took 840ms`,
"expected": { "pass": 1, "unexpected": 1, "random": 0, "skip": 0 },
"expected_images": 2,
},
{ "name": "windows log",
"log": `12:17:14 INFO - REFTEST TEST-START | a == b
12:17:14 INFO - REFTEST TEST-LOAD | a | 1603 / 2053 (78%)
12:17:14 INFO - REFTEST TEST-LOAD | b | 1603 / 2053 (78%)
12:17:14 INFO - REFTEST TEST-PASS(EXPECTED RANDOM) | a == b | image comparison, max difference: 0, number of differing pixels: 0
12:17:14 INFO - REFTEST TEST-END | a == b
12:17:14 INFO - REFTEST TEST-START | a2 == b2
12:17:14 INFO - REFTEST TEST-LOAD | a2 | 1604 / 2053 (78%)
12:17:14 INFO - REFTEST TEST-LOAD | b2 | 1604 / 2053 (78%)
12:17:14 INFO - REFTEST TEST-UNEXPECTED-FAIL | a2 == b2 | image comparison, max difference: 255, number of differing pixels: 9976
12:17:14 INFO - REFTEST IMAGE 1 (TEST): data:image/png;base64,
12:17:14 INFO - REFTEST IMAGE 2 (REFERENCE): data:image/png;base64,
12:17:14 INFO - REFTEST INFO | Saved log: stuff trimmed here
12:17:14 INFO - REFTEST TEST-END | a2 == b2
12:01:09 INFO - REFTEST TEST-START | a3 == b3
12:01:09 INFO - REFTEST TEST-LOAD | a3 | 66 / 189 (34%)
12:01:09 INFO - REFTEST TEST-LOAD | b3 | 66 / 189 (34%)
12:01:09 INFO - REFTEST TEST-KNOWN-FAIL | a3 == b3 | image comparison, max difference: 255, number of differing pixels: 9654
12:01:09 INFO - REFTEST TEST-END | a3 == b3`,
"expected": { "pass": 1, "unexpected": 1, "random": 1, "skip": 0 },
"expected_images": 2,
},
{ "name": "webrender wrench log (windows)",
"log": `[task 2018-12-29T04:29:48.800Z] REFTEST a == b
[task 2018-12-29T04:29:48.984Z] REFTEST a2 == b2
[task 2018-12-29T04:29:49.053Z] REFTEST TEST-UNEXPECTED-FAIL | a2 == b2 | image comparison, max difference: 255, number of differing pixels: 3128
[task 2018-12-29T04:29:49.053Z] REFTEST IMAGE 1 (TEST): data:image/png;
[task 2018-12-29T04:29:49.053Z] REFTEST IMAGE 2 (REFERENCE): data:image/png;
[task 2018-12-29T04:29:49.053Z] REFTEST TEST-END | a2 == b2`,
"expected": { "pass": 0, "unexpected": 1, "random": 0, "skip": 0 },
"expected_images": 2,
},
{ "name": "wpt reftests (Linux local; Bug 1530008)",
"log": `SUITE-START | Running 1 tests
TEST-START | /css/css-backgrounds/border-image-6.html
TEST-UNEXPECTED-FAIL | /css/css-backgrounds/border-image-6.html | Testing http://web-platform.test:8000/css/css-backgrounds/border-image-6.html == http://web-platform.test:8000/css/css-backgrounds/border-image-6-ref.html
REFTEST IMAGE 1 (TEST): data:image/png;base64,
REFTEST IMAGE 2 (REFERENCE): data:image/png;base64,
TEST-INFO took 425ms
SUITE-END | took 2s`,
"expected": { "pass": 0, "unexpected": 1, "random": 0, "skip": 0 },
"expected_images": 2,
},
{ "name": "wpt reftests (taskcluster log from macOS CI)",
"log": `[task 2020-06-26T01:35:29.065Z] 01:35:29 INFO - TEST-START | /html/rendering/non-replaced-elements/the-page/iframe-scrolling-attribute-values.html
[task 2020-06-26T01:35:29.065Z] 01:35:29 INFO - PID 1353 | 1593135329040 Marionette INFO Testing http://web-platform.test:8000/html/rendering/non-replaced-elements/the-page/iframe-scrolling-attribute-values.html == http://web-platform.test:8000/html/rendering/non-replaced-elements/the-page/iframe-scrolling-attribute-values-ref.html
[task 2020-06-26T01:35:29.673Z] 01:35:29 INFO - PID 1353 | 1593135329633 Marionette INFO No differences allowed
[task 2020-06-26T01:35:29.726Z] 01:35:29 INFO - TEST-KNOWN-INTERMITTENT-FAIL | /html/rendering/non-replaced-elements/the-page/iframe-scrolling-attribute-values.html | took 649ms
[task 2020-06-26T01:35:29.726Z] 01:35:29 INFO - REFTEST IMAGE 1 (TEST): data:image/png;
[task 2020-06-26T01:35:29.726Z] 01:35:29 INFO - REFTEST IMAGE 2 (REFERENCE): data:image/png;`,
"expected": { "pass": 0, "unexpected": 0, "random": 1, "skip": 0 },
"expected_images": 2,
},
{ "name": "wpt reftests (taskcluster log from Windows CI)",
"log": `[task 2020-06-26T01:41:19.205Z] 01:41:19 INFO - TEST-START | /html/rendering/non-replaced-elements/the-page/iframe-scrolling-attribute-values.html
[task 2020-06-26T01:41:19.214Z] 01:41:19 INFO - PID 5920 | 1593135679202 Marionette WARN [24] http://web-platform.test:8000/css/WOFF2/metadatadisplay-schema-license-022-ref.xht overflows viewport (width: 783, height: 731)
[task 2020-06-26T01:41:19.214Z] 01:41:19 INFO - PID 9692 | 1593135679208 Marionette INFO Testing http://web-platform.test:8000/html/rendering/non-replaced-elements/the-page/iframe-scrolling-attribute-values.html == http://web-platform.test:8000/html/rendering/non-replaced-elements/the-page/iframe-scrolling-attribute-values-ref.html
[task 2020-06-26T01:41:19.638Z] 01:41:19 INFO - PID 9692 | 1593135679627 Marionette INFO No differences allowed
[task 2020-06-26T01:41:19.688Z] 01:41:19 INFO - TEST-KNOWN-INTERMITTENT-PASS | /html/rendering/non-replaced-elements/the-page/iframe-scrolling-attribute-values.html | took 474ms
[task 2020-06-26T01:41:19.688Z] 01:41:19 INFO - REFTEST IMAGE 1 (TEST): data:image/png;
[task 2020-06-26T01:41:19.689Z] 01:41:19 INFO - REFTEST IMAGE 2 (REFERENCE): data:image/png;`,
"expected": { "pass": 1, "unexpected": 0, "random": 1, "skip": 0 },
"expected_images": 2,
},
{ "name": "local reftest run with timestamps (Linux; Bug 1167712)",
"log": ` 0:05.21 REFTEST TEST-START | a
0:05.21 REFTEST REFTEST TEST-LOAD | a | 0 / 1 (0%)
0:05.27 REFTEST REFTEST TEST-LOAD | b | 0 / 1 (0%)
0:05.66 REFTEST TEST-UNEXPECTED-FAIL | a | image comparison (==), max difference: 106, number of differing pixels: 800
0:05.67 REFTEST REFTEST IMAGE 1 (TEST): data:image/png;base64,
0:05.67 REFTEST REFTEST IMAGE 2 (REFERENCE): data:image/png;base64,
0:05.73 REFTEST REFTEST TEST-END | a`,
"expected": { "pass": 0, "unexpected": 1, "random": 0, "skip": 0 },
"expected_images": 2,
},
{ "name": "reftest run with whitespace compressed (Treeherder; Bug 1084322)",
"log": ` REFTEST TEST-START | a
REFTEST TEST-LOAD | a | 0 / 1 (0%)
REFTEST TEST-LOAD | b | 0 / 1 (0%)
REFTEST TEST-UNEXPECTED-FAIL | a | image comparison (==), max difference: 106, number of differing pixels: 800
REFTEST REFTEST IMAGE 1 (TEST): data:image/png;base64,
REFTEST REFTEST IMAGE 2 (REFERENCE): data:image/png;base64,
REFTEST REFTEST TEST-END | a`,
"expected": { "pass": 0, "unexpected": 1, "random": 0, "skip": 0 },
"expected_images": 2,
},
];
var current_test = 0;
// Override the build_viewer function invoked at the end of process_log to
// actually just check the results of parsing.
build_viewer = function() {
var expected = testcases[current_test].expected;
var expected_images = testcases[current_test].expected_images;
for (var result of gTestItems) {
for (let type in expected) { // type is "pass", "unexpected" etc.
if (result[type]) {
expected[type]--;
}
}
}
var failed = false;
for (let type in expected) {
if (expected[type] != 0) {
console.log(`Failure: for testcase ${testcases[current_test].name} got ${expected[type]} fewer ${type} results than expected!`);
failed = true;
}
}
let total_images = 0;
for (var result of gTestItems) {
total_images += result.images.length;
}
if (total_images !== expected_images) {
console.log(`Failure: for testcase ${testcases[current_test].name} got ${total_images} images, expected ${expected_images}`);
failed = true;
}
if (!failed) {
console.log(`Success for testcase ${testcases[current_test].name}`);
}
};
while (current_test < testcases.length) {
process_log(testcases[current_test].log);
current_test++;
}
}
function process_log(contents) {
var lines = contents.split(/[\r\n]+/);
gTestItems = [];
for (var j in lines) {
// !!!!!!
// When making any changes to this code, please add a test to the
// test_parsing function above, and ensure all existing tests pass.
// !!!!!!
var line = lines[j];
// Ignore duplicated output in logcat.
if (line.match(/I\/Gecko.*?REFTEST/))
continue;
var match = line.match(/^.*?(?:REFTEST\s+)+(.*)$/);
if (!match) {
// WPT reftests don't always have the "REFTEST" prefix but do have
// mozharness prefixing. Trying to match both prefixes optionally with a
// single regex either makes an unreadable mess or matches everything so
// we do them separately.
match = line.match(/^(?:.*? (?:INFO|ERROR) -\s+)(.*)$/);
}
if (match)
line = match[1];
match = line.match(/^(TEST-PASS|TEST-UNEXPECTED-PASS|TEST-FAIL|TEST-KNOWN-FAIL|TEST-UNEXPECTED-FAIL|TEST-DEBUG-INFO|TEST-KNOWN-INTERMITTENT-FAIL|TEST-KNOWN-INTERMITTENT-PASS)(\(EXPECTED RANDOM\)|) \| ([^\|]+)(?: \|(.*)|$)/);
if (match) {
var state = match[1];
var random = match[2];
var url = match[3];
var extra = match[4];
gTestItems.push(
{
pass: !state.match(/DEBUG-INFO$|FAIL$/),
// only one of the following three should ever be true
unexpected: !!state.match(/^TEST-UNEXPECTED/),
random: (random == "(EXPECTED RANDOM)" || state == "TEST-KNOWN-INTERMITTENT-FAIL" || state == "TEST-KNOWN-INTERMITTENT-PASS"),
skip: (extra == " (SKIP)"),
url: url,
images: [],
imageLabels: []
});
continue;
}
match = line.match(/^IMAGE([^:]*): (data:.*)$/);
if (match) {
var item = gTestItems[gTestItems.length - 1];
item.images.push(match[2]);
item.imageLabels.push(match[1]);
}
}
build_viewer();
}
function build_viewer() {
if (gTestItems.length == 0) {
show_phase("entry");
return;
}
var cell = ID("itemlist");
while (cell.childNodes.length > 0)
cell.removeChild(cell.childNodes[cell.childNodes.length - 1]);
var table = document.createElement("table");
var tbody = document.createElement("tbody");
table.appendChild(tbody);
for (var i in gTestItems) {
var item = gTestItems[i];
// optional url filter for only showing unexpected results
if (parseInt(gParams.only_show_unexpected) && !item.unexpected)
continue;
// XXX regardless skip expected pass items until we have filtering UI
if (item.pass && !item.unexpected)
continue;
var tr = document.createElement("tr");
var rowclass = item.pass ? "pass" : "fail";
var td;
var text;
td = document.createElement("td");
text = "";
if (item.unexpected) { text += "!"; rowclass += " unexpected"; }
if (item.random) { text += "R"; rowclass += " random"; }
if (item.skip) { text += "S"; rowclass += " skip"; }
td.appendChild(document.createTextNode(text));
tr.appendChild(td);
td = document.createElement("td");
td.id = "item" + i;
td.className = "url";
// Only display part of URL after "/mozilla/".
var match = item.url.match(/\/mozilla\/(.*)/);
text = document.createTextNode(match ? match[1] : item.url);
if (item.images.length > 0) {
var a = document.createElement("a");
a.href = "javascript:show_images(" + i + ")";
a.appendChild(text);
td.appendChild(a);
} else {
td.appendChild(text);
}
tr.appendChild(td);
tbody.appendChild(tr);
}
cell.appendChild(table);
show_phase("viewer");
}
function get_image_data(src, whenReady) {
var img = new Image();
img.onload = function() {
var canvas = document.createElement("canvas");
canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight;
var ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0);
whenReady(ctx.getImageData(0, 0, img.naturalWidth, img.naturalHeight));
};
img.src = src;
}
function sync_svg_size(imageData) {
// We need the size of the 'svg' and its 'image' elements to match the size
// of the ImageData objects that we're going to read pixels from or else our
// magnify() function will be very broken.
ID("svg").setAttribute("width", imageData.width);
ID("svg").setAttribute("height", imageData.height);
}
function show_images(i) {
var item = gTestItems[i];
var cell = ID("images");
// Remove activeitem class from any existing elements
var activeItems = document.querySelectorAll(".activeitem");
for (var activeItemIdx = activeItems.length; activeItemIdx-- != 0;) {
activeItems[activeItemIdx].classList.remove("activeitem");
}
ID("item" + i).classList.add("activeitem");
ID("image1").style.display = "";
ID("image2").style.display = "none";
ID("diffrect").style.display = "none";
ID("imgcontrols").reset();
ID("pixel-differences").textContent = "";
ID("image1").setAttributeNS(XLINK_NS, "xlink:href", item.images[0]);
// Making the href be #image1 doesn't seem to work
ID("feimage1").setAttributeNS(XLINK_NS, "xlink:href", item.images[0]);
if (item.images.length == 1) {
ID("imgcontrols").style.display = "none";
} else {
ID("imgcontrols").style.display = "";
ID("image2").setAttributeNS(XLINK_NS, "xlink:href", item.images[1]);
// Making the href be #image2 doesn't seem to work
ID("feimage2").setAttributeNS(XLINK_NS, "xlink:href", item.images[1]);
ID("label1").textContent = 'Image ' + item.imageLabels[0];
ID("label2").textContent = 'Image ' + item.imageLabels[1];
}
cell.style.display = "";
let loaded = [false, false];
function images_loaded(id) {
loaded[id] = true;
if (loaded.every(x => x)) {
update_pixel_difference_text()
}
}
get_image_data(item.images[0], function(data) { gImage1Data = data; sync_svg_size(gImage1Data); images_loaded(0)});
get_image_data(item.images[1], function(data) { gImage2Data = data; images_loaded(1)});
}
function update_pixel_difference_text() {
let differenceText;
if (gImage1Data.height !== gImage2Data.height ||
gImage1Data.width !== gImage2Data.width) {
differenceText = "Images are different sizes"
} else {
let [numPixels, maxPerChannel] = get_pixel_differences();
if (!numPixels) {
differenceText = "Images are identical";
} else {
differenceText = `Maximum difference per channel ${maxPerChannel}, ${numPixels} pixels differ`;
}
}
// Disable this for now, because per bug 1633504, the numbers may be
// inaccurate and dependent on the browser's configuration.
// ID("pixel-differences").textContent = differenceText;
}
function get_pixel_differences() {
let numPixels = 0;
let maxPerChannel = 0;
for (var i=0; i<gImage1Data.data.length; i+=4) {
let r1 = gImage1Data.data[i];
let r2 = gImage2Data.data[i];
let g1 = gImage1Data.data[i+1];
let g2 = gImage2Data.data[i+1];
let b1 = gImage1Data.data[i+2];
let b2 = gImage2Data.data[i+2];
// Ignore alpha.
if (r1 == r2 && g1 == g2 && b1 == b2) {
continue;
}
numPixels += 1;
let maxDiff = Math.max(Math.abs(r1-r2),
Math.abs(g1-g2),
Math.abs(b1-b2));
if (maxDiff > maxPerChannel) {
maxPerChannel = maxDiff
}
}
return [numPixels, maxPerChannel];
}
function show_image(i) {
if (i == 1) {
ID("image1").style.display = "";
ID("image2").style.display = "none";
} else {
ID("image1").style.display = "none";
ID("image2").style.display = "";
}
}
function handle_keyboard_shortcut(event) {
switch (event.charCode) {
case 49: // "1" key
document.getElementById("radio1").checked = true;
show_image(1);
break;
case 50: // "2" key
document.getElementById("radio2").checked = true;
show_image(2);
break;
case 100: // "d" key
document.getElementById("differences").click();
break;
case 112: // "p" key
shift_images(-1);
break;
case 110: // "n" key
shift_images(1);
break;
}
}
function handle_keydown(event) {
switch (event.keyCode) {
case 37: // left arrow
move_pixel(-1, 0);
break;
case 38: // up arrow
move_pixel(0,-1);
break;
case 39: // right arrow
move_pixel(1, 0);
break;
case 40: // down arrow
move_pixel(0, 1);
break;
}
}
function shift_images(dir) {
var activeItem = document.querySelector(".activeitem");
if (!activeItem) {
return;
}
for (var elm = activeItem; elm; elm = elm.parentElement) {
if (elm.tagName != "tr") {
continue;
}
elm = dir > 0 ? elm.nextElementSibling : elm.previousElementSibling;
if (elm) {
elm.getElementsByTagName("a")[0].click();
}
return;
}
}
function show_differences(cb) {
ID("diffrect").style.display = cb.checked ? "" : "none";
}
function flash_pixels(on) {
var stroke = on ? "red" : "black";
var strokeWidth = on ? "2px" : "1px";
for (var i = 0; i < gFlashingPixels.length; i++) {
gFlashingPixels[i].setAttribute("stroke", stroke);
gFlashingPixels[i].setAttribute("stroke-width", strokeWidth);
}
}
function cursor_point(evt) {
var m = evt.target.getScreenCTM().inverse();
var p = ID("svg").createSVGPoint();
p.x = evt.clientX;
p.y = evt.clientY;
p = p.matrixTransform(m);
return { x: Math.floor(p.x), y: Math.floor(p.y) };
}
function hex2(i) {
return (i < 16 ? "0" : "") + i.toString(16);
}
function canvas_pixel_as_hex(data, x, y) {
var offset = (y * data.width + x) * 4;
var r = data.data[offset];
var g = data.data[offset + 1];
var b = data.data[offset + 2];
return "#" + hex2(r) + hex2(g) + hex2(b);
}
function hex_as_rgb(hex) {
return "rgb(" + [parseInt(hex.substring(1, 3), 16), parseInt(hex.substring(3, 5), 16), parseInt(hex.substring(5, 7), 16)] + ")";
}
function magnify(evt) {
var { x: x, y: y } = cursor_point(evt);
do_magnify(x, y);
}
function do_magnify(x, y) {
var centerPixelColor1, centerPixelColor2;
var dx_lo = -Math.floor(gMagWidth / 2);
var dx_hi = Math.floor(gMagWidth / 2);
var dy_lo = -Math.floor(gMagHeight / 2);
var dy_hi = Math.floor(gMagHeight / 2);
flash_pixels(false);
gFlashingPixels = [];
for (var j = dy_lo; j <= dy_hi; j++) {
for (var i = dx_lo; i <= dx_hi; i++) {
var px = x + i;
var py = y + j;
var p1 = gMagPixPaths[i + dx_hi][j + dy_hi][0];
var p2 = gMagPixPaths[i + dx_hi][j + dy_hi][1];
// Here we just use the dimensions of gImage1Data since we expect test
// and reference to have the same dimensions.
if (px < 0 || py < 0 || px >= gImage1Data.width || py >= gImage1Data.height) {
p1.setAttribute("fill", "#aaa");
p2.setAttribute("fill", "#888");
} else {
var color1 = canvas_pixel_as_hex(gImage1Data, x + i, y + j);
var color2 = canvas_pixel_as_hex(gImage2Data, x + i, y + j);
p1.setAttribute("fill", color1);
p2.setAttribute("fill", color2);
if (color1 != color2) {
gFlashingPixels.push(p1, p2);
p1.parentNode.appendChild(p1);
p2.parentNode.appendChild(p2);
}
if (i == 0 && j == 0) {
centerPixelColor1 = color1;
centerPixelColor2 = color2;
}
}
}
}
flash_pixels(true);
show_pixelinfo(x, y, centerPixelColor1, hex_as_rgb(centerPixelColor1), centerPixelColor2, hex_as_rgb(centerPixelColor2));
}
function show_pixelinfo(x, y, pix1rgb, pix1hex, pix2rgb, pix2hex) {
var pixelinfo = ID("pixelinfo");
ID("coords").textContent = [x, y];
ID("pix1hex").textContent = pix1hex;
ID("pix1rgb").textContent = pix1rgb;
ID("pix2hex").textContent = pix2hex;
ID("pix2rgb").textContent = pix2rgb;
}
function move_pixel(deltax, deltay) {
coords = ID("coords").textContent.split(',');
x = parseInt(coords[0]);
y = parseInt(coords[1]);
if (isNaN(x) || isNaN(y)) {
return;
}
x = x + deltax;
y = y + deltay;
if (x >= 0 && y >= 0 && x < gImage1Data.width && y < gImage1Data.height) {
do_magnify(x, y);
}
}
]]></script>
</head>
<body onload="load()">
<div id="entry">
<h1>Reftest analyzer: load reftest log</h1>
<p>Either paste your log into this textarea:<br />
<textarea cols="80" rows="10" id="logentry"/><br/>
<input type="button" value="Process pasted log" onclick="log_pasted()" /></p>
<p>... or load it from a file:<br/>
<input type="file" id="fileentry" onchange="fileentry_changed()" />
</p>
</div>
<div id="loading" style="display:none">Loading log...</div>
<div id="viewer" style="display:none">
<div id="pixelarea">
<div id="pixelinfo">
<table>
<tbody>
<tr><th>Pixel at:</th><td colspan="2" id="coords"/></tr>
<tr><th>Image 1:</th><td id="pix1rgb"></td><td id="pix1hex"></td></tr>
<tr><th>Image 2:</th><td id="pix2rgb"></td><td id="pix2hex"></td></tr>
</tbody>
</table>
<div>
<div id="pixelhint">★
<div>
<p>Move the mouse over the reftest image on the right to show
magnified pixels on the left. The color information above is for
the pixel centered in the magnified view.</p>
<p>Image 1 is shown in the upper triangle of each pixel and Image 2
is shown in the lower triangle.</p>
</div>
</div>
</div>
</div>
<div id="magnification">
<svg xmlns="http://www.w3.org/2000/svg" width="84" height="84" shape-rendering="optimizeSpeed">
<g id="mag"/>
</svg>
</div>
</div>
<div id="itemlist"></div>
<div id="images" style="display:none">
<form id="imgcontrols">
<input id="radio1" type="radio" name="which" value="0" onchange="show_image(1)" checked="checked" /><label id="label1" title="1" for="radio1">Image 1</label>
<input id="radio2" type="radio" name="which" value="1" onchange="show_image(2)" /><label id="label2" title="2" for="radio2">Image 2</label>
<label><input id="differences" type="checkbox" onchange="show_differences(this)" />Circle differences</label>
</form>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="800" height="1000" id="svg">
<defs>
<!-- use sRGB to avoid loss of data -->
<filter id="showDifferences" x="0%" y="0%" width="100%" height="100%"
style="color-interpolation-filters: sRGB">
<feImage id="feimage1" result="img1" xlink:href="#image1" />
<feImage id="feimage2" result="img2" xlink:href="#image2" />
<!-- inv1 and inv2 are the images with RGB inverted -->
<feComponentTransfer result="inv1" in="img1">
<feFuncR type="linear" slope="-1" intercept="1" />
<feFuncG type="linear" slope="-1" intercept="1" />
<feFuncB type="linear" slope="-1" intercept="1" />
</feComponentTransfer>
<feComponentTransfer result="inv2" in="img2">
<feFuncR type="linear" slope="-1" intercept="1" />
<feFuncG type="linear" slope="-1" intercept="1" />
<feFuncB type="linear" slope="-1" intercept="1" />
</feComponentTransfer>
<!-- w1 will have non-white pixels anywhere that img2
is brighter than img1, and w2 for the reverse.
It would be nice not to have to go through these
intermediate states, but feComposite
type="arithmetic" can't transform the RGB channels
and leave the alpha channel untouched. -->
<feComposite result="w1" in="img1" in2="inv2" operator="arithmetic" k2="1" k3="1" />
<feComposite result="w2" in="img2" in2="inv1" operator="arithmetic" k2="1" k3="1" />
<!-- c1 will have non-black pixels anywhere that img2
is brighter than img1, and c2 for the reverse -->
<feComponentTransfer result="c1" in="w1">
<feFuncR type="linear" slope="-1" intercept="1" />
<feFuncG type="linear" slope="-1" intercept="1" />
<feFuncB type="linear" slope="-1" intercept="1" />
</feComponentTransfer>
<feComponentTransfer result="c2" in="w2">
<feFuncR type="linear" slope="-1" intercept="1" />
<feFuncG type="linear" slope="-1" intercept="1" />
<feFuncB type="linear" slope="-1" intercept="1" />
</feComponentTransfer>
<!-- c will be nonblack (and fully on) for every pixel+component where there are differences -->
<feComposite result="c" in="c1" in2="c2" operator="arithmetic" k2="255" k3="255" />
<!-- a will be opaque for every pixel with differences and transparent for all others -->
<feColorMatrix result="a" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 0 0" />
<!-- a, dilated by 1 pixel -->
<feMorphology result="dila1" in="a" operator="dilate" radius="1" />
<!-- a, dilated by 2 pixels -->
<feMorphology result="dila2" in="dila1" operator="dilate" radius="1" />
<!-- all the pixels in the 2-pixel dilation of a but not in the 1-pixel dilation, to highlight the diffs -->
<feComposite result="highlight" in="dila2" in2="dila1" operator="out" />
<feFlood result="red" flood-color="red" />
<feComposite result="redhighlight" in="red" in2="highlight" operator="in" />
<feFlood result="black" flood-color="black" flood-opacity="0.5" />
<feMerge>
<feMergeNode in="black" />
<feMergeNode in="redhighlight" />
</feMerge>
</filter>
</defs>
<g onmousemove="magnify(evt)">
<image x="0" y="0" width="100%" height="100%" id="image1" />
<image x="0" y="0" width="100%" height="100%" id="image2" />
</g>
<rect id="diffrect" filter="url(#showDifferences)" pointer-events="none" x="0" y="0" width="100%" height="100%" />
</svg>
<div id="pixel-differences"></div>
</div>
</div>
</body>
</html>

98
test/fixtures/wpt/common/utils.js vendored Normal file
View File

@ -0,0 +1,98 @@
/**
* Create an absolute URL from `options` and defaulting unspecified properties to `window.location`.
* @param {Object} options - a `Location`-like object
* @param {string} options.hostname
* @param {string} options.subdomain - prepend subdomain to the hostname
* @param {string} options.port
* @param {string} options.path
* @param {string} options.query
* @param {string} options.hash
* @returns {string}
*/
function make_absolute_url(options) {
var loc = window.location;
var protocol = get(options, "protocol", loc.protocol);
if (protocol[protocol.length - 1] != ":") {
protocol += ":";
}
var hostname = get(options, "hostname", loc.hostname);
var subdomain = get(options, "subdomain");
if (subdomain) {
hostname = subdomain + "." + hostname;
}
var port = get(options, "port", loc.port)
var path = get(options, "path", loc.pathname);
var query = get(options, "query", loc.search);
var hash = get(options, "hash", loc.hash)
var url = protocol + "//" + hostname;
if (port) {
url += ":" + port;
}
if (path[0] != "/") {
url += "/";
}
url += path;
if (query) {
if (query[0] != "?") {
url += "?";
}
url += query;
}
if (hash) {
if (hash[0] != "#") {
url += "#";
}
url += hash;
}
return url;
}
/** @private */
function get(obj, name, default_val) {
if (obj.hasOwnProperty(name)) {
return obj[name];
}
return default_val;
}
/**
* Generate a new UUID.
* @returns {string}
*/
function token() {
var uuid = [to_hex(rand_int(32), 8),
to_hex(rand_int(16), 4),
to_hex(0x4000 | rand_int(12), 4),
to_hex(0x8000 | rand_int(14), 4),
to_hex(rand_int(48), 12)].join("-")
return uuid;
}
/** @private */
function rand_int(bits) {
if (bits < 1 || bits > 53) {
throw new TypeError();
} else {
if (bits >= 1 && bits <= 30) {
return 0 | ((1 << bits) * Math.random());
} else {
var high = (0 | ((1 << (bits - 30)) * Math.random())) * (1 << 30);
var low = 0 | ((1 << 30) * Math.random());
return high + low;
}
}
}
/** @private */
function to_hex(x, length) {
var rv = x.toString(16);
while (rv.length < length) {
rv = "0" + rv;
}
return rv;
}

View File

@ -0,0 +1 @@
Content-Type: text/javascript; charset=utf-8

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<meta charset="utf-8">
<title>A page that sets window.name</title>
<script>
"use strict";
window.onload = () => {
window.name = location.hash.slice(1); // Drop the first '#' character.
window.name = "spices";
};
</script>

View File

@ -0,0 +1,50 @@
/**
* Imports code into a worklet. E.g.
*
* importWorklet(CSS.paintWorklet, {url: 'script.js'});
* importWorklet(CSS.paintWorklet, '(javascript string)');
*
* @param {Worklet} worklet
* @param {(Object|string)} code
*/
function importWorklet(worklet, code) {
let url;
if (typeof code === 'object') {
url = code.url;
} else {
const blob = new Blob([code], {type: 'text/javascript'});
url = URL.createObjectURL(blob);
}
return worklet.addModule(url);
}
/** @private */
async function animationFrames(frames) {
for (let i = 0; i < frames; i++)
await new Promise(requestAnimationFrame);
}
/** @private */
async function workletPainted() {
await animationFrames(2);
}
/**
* To make sure that we take the snapshot at the right time, we do double
* requestAnimationFrame. In the second frame, we take a screenshot, that makes
* sure that we already have a full frame.
*
* @param {Worklet} worklet
* @param {(Object|string)} code
*/
async function importWorkletAndTerminateTestAfterAsyncPaint(worklet, code) {
if (typeof worklet === 'undefined') {
takeScreenshot();
return;
}
await importWorklet(worklet, code);
await workletPainted();
takeScreenshot();
}

View File

@ -0,0 +1 @@
Content-Type: text/javascript; charset=utf-8