Skip to content

Commit

Permalink
Bug 1600851 [wpt PR 20574] - [Import Maps] Migrate Jest-based resolut…
Browse files Browse the repository at this point in the history
…ion tests into JSON-based tests, a=testonly

Automatic update from web-platform-tests
[Import Maps] Migrate Jest-based resolution tests into JSON-based tests

This CL

- Defines JSON test object format (see README.md) that describes
  the configurations and specifiers to be tested,
- Implements the WPT test helper for executing the tests
  based on the JSON test objects (common-test-helper.js),
- Converts Jest-based resolution tests into JSONs
  (with some refinement, and removing test cases depending
  on interoperability issues of underlying URL parsers), and
- Removes imported resolution tests.

The dependency to Blink internals remains after this CL.
Removing this dependency is planned and discussed at
WICG/import-maps#170.

Bug: 1026809, WICG/import-maps#170
Change-Id: I993cc9cd7746d7175142f8296ac434571f5d7157
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1927852
Commit-Queue: Hiroshige Hayashizaki <[email protected]>
Reviewed-by: Domenic Denicola <[email protected]>
Cr-Commit-Position: refs/heads/master@{#721239}

--

wpt-commits: de73fe8cc89c55dea4fee1fc46d98d73dad2b21b
wpt-pr: 20574
  • Loading branch information
hiroshige-g authored and moz-wptsync-bot committed Dec 9, 2019
1 parent bac37e5 commit ac0390d
Show file tree
Hide file tree
Showing 17 changed files with 834 additions and 476 deletions.
79 changes: 79 additions & 0 deletions testing/web-platform/tests/import-maps/common/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# Import maps test JSON format

In this directory, test inputs and expectations are expressed as JSON files.
This is in order to share the same JSON files between WPT tests and Jest-based
tests for the reference JavaScript implementation at [WICG repository](https://github.com/WICG/import-maps/tree/master/reference-implementation).

## Basics

A **test object** describes a set of parameters (import maps and base URLs) and specifiers to be tested.
Each JSON file under [resources/](resources/) directory consists of a test object.
A minimum test object would be:

```json
{
"name": "Main test name",
"importMapBaseURL": "https://example.com/import-map-base-url/index.html",
"importMap": {
"imports": {
"a": "/mapped-a.mjs"
}
},
"baseURL": "https://example.com/base-url/app.mjs",
"expectedResults": {
"a": "https://example.com/mapped-a.mjs",
"b": null
}
}
```

Required fields:

- `name`: Test name.
- In WPT tests, this is used for the test name of `promise_test()` together with specifier to be resolved, like `"Main test name: a"`.
- `importMap` (object or string): the import map to be attached.
- `importMapBaseURL` (string): the base URL used for [parsing the import map](https://wicg.github.io/import-maps/#parse-an-import-map-string).
- `baseURL` (string): the base URL used in [resolving a specifier](https://wicg.github.io/import-maps/#resolve-a-module-specifier) for each specifiers.
- `expectedResults` (object; string to (string or null)): test cases.
- The keys are specifiers to be resolved.
- The values are expected resolved URLs. If `null`, resolution should fail.

Optional fields:

- `link` and `details` can be used for e.g. linking to specs or adding more detailed descriptions.
- Currently they are simply ignored by the WPT test helper.

## Nesting and inheritance

We can organize tests by nesting test objects.
A test object can contain child test objects (*subtests*) using `tests` field.
The Keys of the `tests` value are the names of subtests, and values are test objects.

For example:

```json
{
"name": "Main test name",
"importMapBaseURL": "https://example.com/import-map-base-url/index.html",
"importMap": {
"imports": {
"a": "/mapped-a.mjs"
}
},
"tests": {
"Subtest1": {
"baseURL": "https://example.com/base-url1/app.mjs",
"expectedResults": { "a": "https://example.com/mapped-a.mjs" }
},
"Subtest2": {
"baseURL": "https://example.com/base-url2/app.mjs",
"expectedResults": { "b": null }
}
}
}
```

The top-level test object contains two sub test objects, named as `Subtest1` and `Subtest2`, respectively.

Child test objects inherit fields from their parent test object.
In the example above, the child test objects specifies `baseURL` fields, while they inherits other fields (e.g. `importMapBaseURL`) from the top-level test object.
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<!DOCTYPE html>
<meta name="timeout" content="long">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<body>
<script type="module">
import { runTestsFromJSON } from "./resources/common-test-helper.js";

for (const json of [
'resources/scopes.json',
'resources/empty-import-map.json',
'resources/packages-via-trailing-slashes.json',
'resources/tricky-specifiers.json',
'resources/url-specifiers.json',
'resources/data-base-url.json',
'resources/scopes-exact-vs-prefix.json',
'resources/overlapping-entries.json',
]) {
promise_test(() =>
runTestsFromJSON(json),
"Test helper: fetching and sanity checking test JSON: " + json);
}
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
setup({allow_uncaught_exception : true});

// Creates a new Document (via <iframe>) and add an inline import map.
function parse(importMap, importMapBaseURL) {
return new Promise((resolve, reject) => {
const importMapString = JSON.stringify(importMap);
const iframe = document.createElement('iframe');

window.addEventListener('message', event => {
if (event.data.type === 'Success') {
resolve(iframe);
} else {
// Currently we don't distinguish fetch errors and parse errors.
reject(event.data.error);
}
},
{once: true});

const testHTML = `
<script>
// Handle errors around fetching, parsing and registering import maps.
let registrationResult;
const onScriptError = event => {
registrationResult = {type: 'FetchError', error: event.error};
return false;
};
const windowErrorHandler = event => {
registrationResult = {type: 'ParseError', error: event.error};
return false;
};
window.addEventListener('error', windowErrorHandler);
window.addEventListener('load', event => {
if (!registrationResult) {
registrationResult = {type: 'Success'};
}
window.removeEventListener('error', windowErrorHandler);
parent.postMessage(registrationResult, '*');
});
// Handle specifier resolution requests from the parent frame.
window.addEventListener('message', event => {
try {
// URL resolution is tested using Chromium's internals.
// TODO(hiroshige): Remove the Chromium-specific dependency.
const result = internals.resolveModuleSpecifier(
event.data.specifier,
event.data.baseURL,
document);
parent.postMessage({type: 'ResolutionSuccess', result: result}, '*');
} catch (e) {
// We post error names instead of error objects themselves and
// re-create error objects later, to avoid issues around serializing
// error objects which is a quite new feature.
parent.postMessage({type: 'ResolutionFailure', result: e.name}, '*');
}
});
</script>
<script type="importmap" onerror="onScriptError(event)">
${importMapString}
</script>
`;

if (new URL(importMapBaseURL).protocol === 'data:') {
iframe.src = 'data:text/html;base64,' + btoa(testHTML);
} else {
iframe.srcdoc = `<base href="${importMapBaseURL}">` + testHTML;
}

document.body.appendChild(iframe);

});
}

// Returns a promise that is resolved with the resulting URL.
function resolve(specifier, parsedImportMap, baseURL) {
return new Promise((resolve, reject) => {
window.addEventListener('message', event => {
if (event.data.type === 'ResolutionSuccess') {
resolve(event.data.result);
} else if (event.data.type === 'ResolutionFailure') {
if (event.data.result === 'TypeError') {
reject(new TypeError());
} else {
reject(new Error(event.data.result));
}
} else {
assert_unreached('Invalid message: ' + event.data.type);
}
},
{once: true});

parsedImportMap.contentWindow.postMessage(
{specifier: specifier, baseURL: baseURL}, '*');
});
}

function assert_no_extra_properties(object, expectedProperties, description) {
for (const actualProperty in object) {
assert_true(expectedProperties.indexOf(actualProperty) !== -1,
description + ": unexpected property " + actualProperty);
}
}

async function runTests(j) {
const tests = j.tests;
delete j.tests;

if (j.importMap) {
assert_own_property(j, 'importMap');
assert_own_property(j, 'importMapBaseURL');
j.parsedImportMap = await parse(j.importMap, j.importMapBaseURL);
delete j.importMap;
delete j.importMapBaseURL;
}

assert_no_extra_properties(
j,
['expectedResults', 'baseURL', 'name', 'parsedImportMap',
'importMap', 'importMapBaseURL',
'link', 'details'],
j.name);

if (tests) {
// Nested node.
for (const testName in tests) {
let fullTestName = testName;
if (j.name) {
fullTestName = j.name + ': ' + testName;
}
tests[testName].name = fullTestName;
const k = Object.assign(Object.assign({}, j), tests[testName]);
await runTests(k);
}
} else {
// Leaf node.
for (const key of
['expectedResults', 'parsedImportMap', 'baseURL', 'name']) {
assert_own_property(j, key, j.name);
}

for (const specifier in j.expectedResults) {
const expected = j.expectedResults[specifier];
promise_test(async t => {
if (expected === null) {
return promise_rejects(t, new TypeError(),
resolve(specifier, j.parsedImportMap, j.baseURL));
} else {
// Should be resolved to `expected`.
const actual = await resolve(
specifier, j.parsedImportMap, j.baseURL);
assert_equals(actual, expected);
}
},
j.name + ': ' + specifier);
}
}
}

export async function runTestsFromJSON(jsonURL) {
const response = await fetch(jsonURL);
const json = await response.json();
await runTests(json);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"importMap": {
"imports": {
"foo/": "data:text/javascript,foo/"
}
},
"importMapBaseURL": "https://example.com/app/index.html",
"baseURL": "https://example.com/js/app.mjs",
"name": "data: base URL (?)",
"tests": {
"should favor the most-specific key": {
"expectedResults": {
"foo/bar": null
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
{
"importMap": {},
"importMapBaseURL": "https://example.com/app/index.html",
"baseURL": "https://example.com/js/app.mjs",
"tests": {
"valid relative specifiers": {
"expectedResults": {
"./foo": "https://example.com/js/foo",
"./foo/bar": "https://example.com/js/foo/bar",
"./foo/../bar": "https://example.com/js/bar",
"./foo/../../bar": "https://example.com/bar",
"../foo": "https://example.com/foo",
"../foo/bar": "https://example.com/foo/bar",
"../../../foo/bar": "https://example.com/foo/bar",
"/foo": "https://example.com/foo",
"/foo/bar": "https://example.com/foo/bar",
"/../../foo/bar": "https://example.com/foo/bar",
"/../foo/../bar": "https://example.com/bar"
}
},
"fetch scheme absolute URLs": {
"expectedResults": {
"about:fetch-scheme": "about:fetch-scheme",
"https://fetch-scheme.net": "https://fetch-scheme.net/",
"https:fetch-scheme.org": "https://fetch-scheme.org/",
"https://fetch%2Dscheme.com/": "https://fetch-scheme.com/",
"https://///fetch-scheme.com///": "https://fetch-scheme.com///"
}
},
"non-fetch scheme absolute URLs": {
"expectedResults": {
"mailto:non-fetch-scheme": "mailto:non-fetch-scheme",
"import:non-fetch-scheme": "import:non-fetch-scheme",
"javascript:non-fetch-scheme": "javascript:non-fetch-scheme",
"wss:non-fetch-scheme": "wss://non-fetch-scheme/"
}
},
"valid relative URLs that are invalid as specifiers should fail": {
"expectedResults": {
"invalid-specifier": null,
"\\invalid-specifier": null,
":invalid-specifier": null,
"@invalid-specifier": null,
"%2E/invalid-specifier": null,
"%2E%2E/invalid-specifier": null,
".%2Finvalid-specifier": null
}
},
"invalid absolute URLs should fail": {
"expectedResults": {
"https://invalid-url.com:demo": null,
"http://[invalid-url.com]/": null
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"importMapBaseURL": "https://example.com/app/index.html",
"baseURL": "https://example.com/js/app.mjs",
"name": "should favor the most-specific key",
"tests": {
"Overlapping entries with trailing slashes": {
"importMap": {
"imports": {
"a": "/1",
"a/": "/2/",
"a/b": "/3",
"a/b/": "/4/"
}
},
"expectedResults": {
"a": "https://example.com/1",
"a/": "https://example.com/2/",
"a/x": "https://example.com/2/x",
"a/b": "https://example.com/3",
"a/b/": "https://example.com/4/",
"a/b/c": "https://example.com/4/c"
}
}
}
}
Loading

0 comments on commit ac0390d

Please sign in to comment.