Skip to content

Commit

Permalink
Import maps: use wpt_internal for parsing unit tests
Browse files Browse the repository at this point in the history
https://chromium-review.googlesource.com/c/chromium/src/+/3816001/ removed the test coverage in wpt/external for parsing import maps. Although we plan to eventually convert those tests into resolution tests, it's still nice to have unit test-like coverage for our import map parser, so this CL copies over the data files in their current form and creates a small harness for directly testing them.

Change-Id: I40837cb336e721e6e52c5f959a46c9220e225edf
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3815111
Commit-Queue: Domenic Denicola <domenic@chromium.org>
Reviewed-by: Hiroshige Hayashizaki <hiroshige@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1032873}
  • Loading branch information
domenic authored and Chromium LUCI CQ committed Aug 9, 2022
1 parent 53f9f89 commit 5c88b45
Show file tree
Hide file tree
Showing 13 changed files with 942 additions and 0 deletions.
15 changes: 15 additions & 0 deletions third_party/blink/web_tests/wpt_internal/import-maps/README.md
@@ -0,0 +1,15 @@
# Import maps internal tests

These tests are essentially unit tests of the import map parser. The only
observable effect of an import map is how it impacts module resolution, which is
tested in `external/wpt`. But it's nice to have these sorts of parsing unit
tests too.

## Format

The format is similar to the resolution tests format, which is described in
`external/wpt/import-maps/README.md`. The only differences are that for a given
test object:

* Instead of `expectedResults`, we have `expectedParsedImportMap`.
* `baseURL` is not applicable and cannot appear.
111 changes: 111 additions & 0 deletions third_party/blink/web_tests/wpt_internal/import-maps/parsing.html
@@ -0,0 +1,111 @@
<!DOCTYPE html>
<meta charset="utf-8">

<meta name="variant" content="?parsing-addresses-absolute.json">
<meta name="variant" content="?parsing-addresses-invalid.json">
<meta name="variant" content="?parsing-addresses.json">
<meta name="variant" content="?parsing-invalid-json.json">
<meta name="variant" content="?parsing-schema-normalization.json">
<meta name="variant" content="?parsing-schema-scope.json">
<meta name="variant" content="?parsing-schema-specifier-map.json">
<meta name="variant" content="?parsing-schema-toplevel.json">
<meta name="variant" content="?parsing-scope-keys.json">
<meta name="variant" content="?parsing-specifier-keys.json">
<meta name="variant" content="?parsing-trailing-slashes.json">

<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>

<script>

const filename = location.search.substring(1);
promise_test(async () => {
const res = await fetch('resources/' + filename);
const json = await res.json();
const tests = await jsonToTests(json);

for (const test of tests) {
promise_test(async t => {
const testHTML = `
<!DOCTYPE html>
<base href="${test.importMapBaseURL}">
<script type="importmap">
${JSON.stringify(test.importMap)}
</scri` + `pt>
<script>
parent.postMessage(internals.getParsedImportMap(document), '*');
</scri` + `pt>
`;

const iframe = document.createElement('iframe');
if (new URL(test.importMapBaseURL).protocol === 'data:') {
iframe.src = 'data:text/html;base64,' + btoa(testHTML);
} else {
iframe.src = '/common/blank.html';
iframe.onload = () => {
iframe.contentDocument.write(testHTML);
iframe.contentDocument.close();
};
}
document.body.append(iframe);

let parsedImportMap = JSON.parse(await new Promise(resolve => {
window.onmessage = t.step_func(e => {
assert_equals(e.source, iframe.contentWindow);
resolve(e.data);
});
}));

// internals.getParsedImportMap() returns an empty object instead of null for parse failures.
// Translate it so the test comparison works as expected.
if (Object.keys(parsedImportMap).length === 0) {
parsedImportMap = null;
}

assert_equals(
stringifyImportMap(parsedImportMap),
stringifyImportMap(test.expectedParsedImportMap)
);
}, test.name);
}
}, 'Data fetching and setup');

function jsonToTests(json, inheritedProps = {}, name = '') {
const baseProps = {
importMap: orFallback(json, inheritedProps, 'importMap'),
importMapBaseURL: orFallback(json, inheritedProps, 'importMapBaseURL'),
expectedParsedImportMap: orFallback(json, inheritedProps, 'expectedParsedImportMap')
};

if (json.tests) {
// Parent node
return Object.entries(json.tests).flatMap(([subName, subJSON]) => {
const fullName = [name, json.name, subName].filter(Boolean).join(': ');
return jsonToTests(subJSON, baseProps, fullName);
});
} else {
// Leaf (test) node
return [{ name, ...baseProps }];
}
}

function orFallback(obj1, obj2, property) {
return obj1.hasOwnProperty(property) ? obj1[property] : obj2[property];
}

// Sort keys and then stringify for comparison.
function stringifyImportMap(importMap) {
function getKeys(m) {
if (typeof m !== 'object')
return [];

let keys = [];
for (const key in m) {
keys.push(key);
keys = keys.concat(getKeys(m[key]));
}
return keys;
}
return JSON.stringify(importMap, getKeys(importMap).sort());
}
</script>
@@ -0,0 +1,65 @@
{
"name": "Absolute URL addresses",
"tests": {
"should only accept absolute URL addresses with fetch schemes": {
"importMap": {
"imports": {
"about": "about:good",
"blob": "blob:good",
"data": "data:good",
"file": "file:///good",
"filesystem": "filesystem:http://example.com/good/",
"http": "http://good/",
"https": "https://good/",
"ftp": "ftp://good/",
"import": "import:bad",
"mailto": "mailto:bad",
"javascript": "javascript:bad",
"wss": "wss:bad"
}
},
"importMapBaseURL": "https://base.example/path1/path2/path3",
"expectedParsedImportMap": {
"imports": {
"about": "about:good",
"blob": "blob:good",
"data": "data:good",
"file": "file:///good",
"filesystem": "filesystem:http://example.com/good/",
"http": "http://good/",
"https": "https://good/",
"ftp": "ftp://good/",
"import": "import:bad",
"javascript": "javascript:bad",
"mailto": "mailto:bad",
"wss": "wss://bad/"
},
"scopes": {}
}
},
"should parse absolute URLs, ignoring unparseable ones": {
"importMap": {
"imports": {
"unparseable2": "https://example.com:demo",
"unparseable3": "http://[www.example.com]/",
"invalidButParseable1": "https:example.org",
"invalidButParseable2": "https://///example.com///",
"prettyNormal": "https://example.net",
"percentDecoding": "https://ex%41mple.com/"
}
},
"importMapBaseURL": "https://base.example/path1/path2/path3",
"expectedParsedImportMap": {
"imports": {
"unparseable2": null,
"unparseable3": null,
"invalidButParseable1": "https://example.org/",
"invalidButParseable2": "https://example.com///",
"prettyNormal": "https://example.net/",
"percentDecoding": "https://example.com/"
},
"scopes": {}
}
}
}
}
@@ -0,0 +1,27 @@
{
"name": "Other invalid addresses",
"tests": {
"should ignore unprefixed strings that are not absolute URLs": {
"importMap": {
"imports": {
"foo1": "bar",
"foo2": "\\bar",
"foo3": "~bar",
"foo4": "#bar",
"foo5": "?bar"
}
},
"importMapBaseURL": "https://base.example/path1/path2/path3",
"expectedParsedImportMap": {
"imports": {
"foo1": null,
"foo2": null,
"foo3": null,
"foo4": null,
"foo5": null
},
"scopes": {}
}
}
}
}
@@ -0,0 +1,85 @@
{
"name": "Relative URL-like addresses",
"tests": {
"should accept strings prefixed with ./, ../, or /": {
"importMap": {
"imports": {
"dotSlash": "./foo",
"dotDotSlash": "../foo",
"slash": "/foo"
}
},
"importMapBaseURL": "https://base.example/path1/path2/path3",
"expectedParsedImportMap": {
"imports": {
"dotSlash": "https://base.example/path1/path2/foo",
"dotDotSlash": "https://base.example/path1/foo",
"slash": "https://base.example/foo"
},
"scopes": {}
}
},
"should not accept strings prefixed with ./, ../, or / for data: base URLs": {
"importMap": {
"imports": {
"dotSlash": "./foo",
"dotDotSlash": "../foo",
"slash": "/foo"
}
},
"importMapBaseURL": "data:text/html,test",
"expectedParsedImportMap": {
"imports": {
"dotSlash": null,
"dotDotSlash": null,
"slash": null
},
"scopes": {}
}
},
"should accept the literal strings ./, ../, or / with no suffix": {
"importMap": {
"imports": {
"dotSlash": "./",
"dotDotSlash": "../",
"slash": "/"
}
},
"importMapBaseURL": "https://base.example/path1/path2/path3",
"expectedParsedImportMap": {
"imports": {
"dotSlash": "https://base.example/path1/path2/",
"dotDotSlash": "https://base.example/path1/",
"slash": "https://base.example/"
},
"scopes": {}
}
},
"should ignore percent-encoded variants of ./, ../, or /": {
"importMap": {
"imports": {
"dotSlash1": "%2E/",
"dotDotSlash1": "%2E%2E/",
"dotSlash2": ".%2F",
"dotDotSlash2": "..%2F",
"slash2": "%2F",
"dotSlash3": "%2E%2F",
"dotDotSlash3": "%2E%2E%2F"
}
},
"importMapBaseURL": "https://base.example/path1/path2/path3",
"expectedParsedImportMap": {
"imports": {
"dotSlash1": null,
"dotDotSlash1": null,
"dotSlash2": null,
"dotDotSlash2": null,
"slash2": null,
"dotSlash3": null,
"dotDotSlash3": null
},
"scopes": {}
}
}
}
}
@@ -0,0 +1,6 @@
{
"name": "Invalid JSON",
"importMapBaseURL": "https://base.example/",
"importMap": "{imports: {}}",
"expectedParsedImportMap": null
}
@@ -0,0 +1,31 @@
{
"name": "Normalization",
"importMapBaseURL": "https://base.example/",
"tests": {
"should normalize empty import maps to have imports and scopes keys": {
"importMap": {},
"expectedParsedImportMap": {
"imports": {},
"scopes": {}
}
},
"should normalize an import map without imports to have imports": {
"importMap": {
"scopes": {}
},
"expectedParsedImportMap": {
"imports": {},
"scopes": {}
}
},
"should normalize an import map without scopes to have scopes": {
"importMap": {
"imports": {}
},
"expectedParsedImportMap": {
"imports": {},
"scopes": {}
}
}
}
}
@@ -0,0 +1,46 @@
{
"name": "Mismatching scopes schema",
"importMapBaseURL": "https://base.example/",
"tests": {
"should throw if a scope's value is not an object": {
"expectedParsedImportMap": null,
"tests": {
"null": {
"importMap": {
"scopes": {
"https://example.com/": null
}
}
},
"boolean": {
"importMap": {
"scopes": {
"https://example.com/": true
}
}
},
"number": {
"importMap": {
"scopes": {
"https://example.com/": 1
}
}
},
"string": {
"importMap": {
"scopes": {
"https://example.com/": "foo"
}
}
},
"array": {
"importMap": {
"scopes": {
"https://example.com/": []
}
}
}
}
}
}
}

0 comments on commit 5c88b45

Please sign in to comment.