summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/import-maps
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:22:09 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:22:09 +0000
commit43a97878ce14b72f0981164f87f2e35e14151312 (patch)
tree620249daf56c0258faa40cbdcf9cfba06de2a846 /testing/web-platform/tests/import-maps
parentInitial commit. (diff)
downloadfirefox-43a97878ce14b72f0981164f87f2e35e14151312.tar.xz
firefox-43a97878ce14b72f0981164f87f2e35e14151312.zip
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/web-platform/tests/import-maps')
-rw-r--r--testing/web-platform/tests/import-maps/META.yml4
-rw-r--r--testing/web-platform/tests/import-maps/acquiring/README.md1
-rw-r--r--testing/web-platform/tests/import-maps/acquiring/dynamic-import.html30
-rw-r--r--testing/web-platform/tests/import-maps/acquiring/modulepreload-link-header.html23
-rw-r--r--testing/web-platform/tests/import-maps/acquiring/modulepreload-link-header.html.headers1
-rw-r--r--testing/web-platform/tests/import-maps/acquiring/modulepreload.html24
-rw-r--r--testing/web-platform/tests/import-maps/acquiring/script-tag-inline.html30
-rw-r--r--testing/web-platform/tests/import-maps/acquiring/script-tag.html24
-rw-r--r--testing/web-platform/tests/import-maps/acquiring/worker-request.html22
-rw-r--r--testing/web-platform/tests/import-maps/bare-specifiers.sub.html46
-rw-r--r--testing/web-platform/tests/import-maps/bare/README.md1
-rw-r--r--testing/web-platform/tests/import-maps/bare/__dir__.headers1
-rw-r--r--testing/web-platform/tests/import-maps/bare/bare1
-rw-r--r--testing/web-platform/tests/import-maps/bare/cross-origin-bare1
-rw-r--r--testing/web-platform/tests/import-maps/bare/to-bare1
-rw-r--r--testing/web-platform/tests/import-maps/bare/to-data1
-rw-r--r--testing/web-platform/tests/import-maps/csp/applied-to-target-dynamic.sub.html26
-rw-r--r--testing/web-platform/tests/import-maps/csp/applied-to-target.sub.html31
-rw-r--r--testing/web-platform/tests/import-maps/csp/hash-failure.html24
-rw-r--r--testing/web-platform/tests/import-maps/csp/hash.html24
-rw-r--r--testing/web-platform/tests/import-maps/csp/nonce-failure.html27
-rw-r--r--testing/web-platform/tests/import-maps/csp/nonce.html20
-rw-r--r--testing/web-platform/tests/import-maps/csp/unsafe-inline.html20
-rw-r--r--testing/web-platform/tests/import-maps/data-driven/README.md87
-rw-r--r--testing/web-platform/tests/import-maps/data-driven/resolving.html23
-rw-r--r--testing/web-platform/tests/import-maps/data-driven/resources/data-url-prefix.json17
-rw-r--r--testing/web-platform/tests/import-maps/data-driven/resources/empty-import-map.json61
-rw-r--r--testing/web-platform/tests/import-maps/data-driven/resources/overlapping-entries.json25
-rw-r--r--testing/web-platform/tests/import-maps/data-driven/resources/packages-via-trailing-slashes.json74
-rw-r--r--testing/web-platform/tests/import-maps/data-driven/resources/parsing-addresses-absolute.json65
-rw-r--r--testing/web-platform/tests/import-maps/data-driven/resources/parsing-addresses-invalid.json27
-rw-r--r--testing/web-platform/tests/import-maps/data-driven/resources/parsing-addresses.json85
-rw-r--r--testing/web-platform/tests/import-maps/data-driven/resources/parsing-invalid-json.json6
-rw-r--r--testing/web-platform/tests/import-maps/data-driven/resources/parsing-schema-normalization.json31
-rw-r--r--testing/web-platform/tests/import-maps/data-driven/resources/parsing-schema-scope.json46
-rw-r--r--testing/web-platform/tests/import-maps/data-driven/resources/parsing-schema-specifier-map.json44
-rw-r--r--testing/web-platform/tests/import-maps/data-driven/resources/parsing-schema-toplevel.json97
-rw-r--r--testing/web-platform/tests/import-maps/data-driven/resources/parsing-scope-keys.json191
-rw-r--r--testing/web-platform/tests/import-maps/data-driven/resources/parsing-specifier-keys.json209
-rw-r--r--testing/web-platform/tests/import-maps/data-driven/resources/parsing-trailing-slashes.json15
-rw-r--r--testing/web-platform/tests/import-maps/data-driven/resources/resolving-null.json82
-rw-r--r--testing/web-platform/tests/import-maps/data-driven/resources/scopes-exact-vs-prefix.json134
-rw-r--r--testing/web-platform/tests/import-maps/data-driven/resources/scopes.json171
-rw-r--r--testing/web-platform/tests/import-maps/data-driven/resources/test-helper-iframe.js46
-rw-r--r--testing/web-platform/tests/import-maps/data-driven/resources/test-helper.js142
-rw-r--r--testing/web-platform/tests/import-maps/data-driven/resources/tricky-specifiers.json71
-rw-r--r--testing/web-platform/tests/import-maps/data-driven/resources/url-specifiers-schemes.json45
-rw-r--r--testing/web-platform/tests/import-maps/data-driven/resources/url-specifiers.json68
-rw-r--r--testing/web-platform/tests/import-maps/data-driven/tools/format_json.py27
-rw-r--r--testing/web-platform/tests/import-maps/data-url-specifiers.sub.html48
-rw-r--r--testing/web-platform/tests/import-maps/http-url-like-specifiers.sub.html47
-rw-r--r--testing/web-platform/tests/import-maps/import-maps-base-url.sub.html38
-rw-r--r--testing/web-platform/tests/import-maps/module-map-key.html21
-rw-r--r--testing/web-platform/tests/import-maps/multiple-import-maps/basic.html36
-rw-r--r--testing/web-platform/tests/import-maps/multiple-import-maps/with-errors.html32
-rw-r--r--testing/web-platform/tests/import-maps/not-as-classic-script.html45
-rw-r--r--testing/web-platform/tests/import-maps/resources/empty.js0
-rw-r--r--testing/web-platform/tests/import-maps/resources/inject-base.js3
-rw-r--r--testing/web-platform/tests/import-maps/resources/log.js1
-rw-r--r--testing/web-platform/tests/import-maps/resources/log.js.headers1
-rw-r--r--testing/web-platform/tests/import-maps/resources/test-helper.js246
-rw-r--r--testing/web-platform/tests/import-maps/script-supports-importmap.html22
-rw-r--r--testing/web-platform/tests/import-maps/static-import.py11
63 files changed, 2823 insertions, 0 deletions
diff --git a/testing/web-platform/tests/import-maps/META.yml b/testing/web-platform/tests/import-maps/META.yml
new file mode 100644
index 0000000000..dbcc70edab
--- /dev/null
+++ b/testing/web-platform/tests/import-maps/META.yml
@@ -0,0 +1,4 @@
+spec: https://wicg.github.io/import-maps/
+suggested_reviewers:
+ - domenic
+ - hiroshige-g
diff --git a/testing/web-platform/tests/import-maps/acquiring/README.md b/testing/web-platform/tests/import-maps/acquiring/README.md
new file mode 100644
index 0000000000..189d190217
--- /dev/null
+++ b/testing/web-platform/tests/import-maps/acquiring/README.md
@@ -0,0 +1 @@
+These tests are about the impact of the [acquiring import maps](https://wicg.github.io/import-maps/#document-acquiring-import-maps) boolean, which prevents import maps from taking effect after a module import has started.
diff --git a/testing/web-platform/tests/import-maps/acquiring/dynamic-import.html b/testing/web-platform/tests/import-maps/acquiring/dynamic-import.html
new file mode 100644
index 0000000000..49d8a701da
--- /dev/null
+++ b/testing/web-platform/tests/import-maps/acquiring/dynamic-import.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+const t = async_test(
+ 'After dynamic imports, import maps should fire error events');
+const log = [];
+// To ensure we are testing that the flag is cleared at the beginning of module
+// script loading unconditionally, not at the end of loading or not at the
+// first attempt to resolve a module specifier, trickle(d1) is used to ensure
+// the following import map is added after module loading is triggered but
+// before the first module script is parsed.
+promise_test(() => import('../resources/empty.js?pipe=trickle(d1)'),
+ "A dynamic import succeeds");
+</script>
+<script type="importmap" onload="t.assert_unreached('onload')" onerror="t.done()">
+{
+ "imports": {
+ "../resources/log.js?pipe=sub&name=A": "../resources/log.js?pipe=sub&name=B"
+ }
+}
+</script>
+<script>
+promise_test(() => {
+ return import("../resources/log.js?pipe=sub&name=A")
+ .then(() => assert_array_equals(log, ["log:A"]))
+ },
+ 'After a dynamic import(), import maps are not effective');
+</script>
diff --git a/testing/web-platform/tests/import-maps/acquiring/modulepreload-link-header.html b/testing/web-platform/tests/import-maps/acquiring/modulepreload-link-header.html
new file mode 100644
index 0000000000..dde8cabb93
--- /dev/null
+++ b/testing/web-platform/tests/import-maps/acquiring/modulepreload-link-header.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+const t = async_test(
+ 'With modulepreload link header, import maps should fire error events');
+const log = [];
+</script>
+<script type="importmap" onerror="t.done()">
+{
+ "imports": {
+ "../resources/log.js?pipe=sub&name=A": "../resources/log.js?pipe=sub&name=B"
+ }
+}
+</script>
+<script>
+promise_test(() => {
+ return import("../resources/log.js?pipe=sub&name=A")
+ .then(() => assert_array_equals(log, ["log:A"]))
+ },
+ 'With modulepreload link header, import maps are not effective');
+</script>
diff --git a/testing/web-platform/tests/import-maps/acquiring/modulepreload-link-header.html.headers b/testing/web-platform/tests/import-maps/acquiring/modulepreload-link-header.html.headers
new file mode 100644
index 0000000000..bb81a4c569
--- /dev/null
+++ b/testing/web-platform/tests/import-maps/acquiring/modulepreload-link-header.html.headers
@@ -0,0 +1 @@
+Link: <../resources/empty.js?pipe=trickle(d1)>;rel=modulepreload
diff --git a/testing/web-platform/tests/import-maps/acquiring/modulepreload.html b/testing/web-platform/tests/import-maps/acquiring/modulepreload.html
new file mode 100644
index 0000000000..68b66a8ae3
--- /dev/null
+++ b/testing/web-platform/tests/import-maps/acquiring/modulepreload.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+const t = async_test(
+ 'After <link rel=modulepreload> import maps should fire error events');
+const log = [];
+</script>
+<link rel="modulepreload" href="../resources/empty.js?pipe=trickle(d1)"></link>
+<script type="importmap" onerror="t.done()">
+{
+ "imports": {
+ "../resources/log.js?pipe=sub&name=A": "../resources/log.js?pipe=sub&name=B"
+ }
+}
+</script>
+<script>
+promise_test(() => {
+ return import("../resources/log.js?pipe=sub&name=A")
+ .then(() => assert_array_equals(log, ["log:A"]))
+ },
+ 'After <link rel=modulepreload> import maps are not effective');
+</script>
diff --git a/testing/web-platform/tests/import-maps/acquiring/script-tag-inline.html b/testing/web-platform/tests/import-maps/acquiring/script-tag-inline.html
new file mode 100644
index 0000000000..683ce83c3a
--- /dev/null
+++ b/testing/web-platform/tests/import-maps/acquiring/script-tag-inline.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+const t = async_test(
+ 'After inline <script type="module"> import maps should fire error events');
+const log = [];
+</script>
+<script type="module">
+// While this inline module script doesn't have any specifiers and doesn't fetch
+// anything, this still disables subsequent import maps, because
+// https://wicg.github.io/import-maps/#wait-for-import-maps
+// is anyway called at the beginning of
+// https://html.spec.whatwg.org/multipage/webappapis.html#fetch-an-inline-module-script-graph
+</script>
+<script type="importmap" onerror="t.done()">
+{
+ "imports": {
+ "../resources/log.js?pipe=sub&name=A": "../resources/log.js?pipe=sub&name=B"
+ }
+}
+</script>
+<script>
+promise_test(() => {
+ return import("../resources/log.js?pipe=sub&name=A")
+ .then(() => assert_array_equals(log, ["log:A"]))
+ },
+ 'After inline <script type="module"> import maps are not effective');
+</script>
diff --git a/testing/web-platform/tests/import-maps/acquiring/script-tag.html b/testing/web-platform/tests/import-maps/acquiring/script-tag.html
new file mode 100644
index 0000000000..2792c2a31d
--- /dev/null
+++ b/testing/web-platform/tests/import-maps/acquiring/script-tag.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+const t = async_test(
+ 'After <script type="module"> import maps should fire error events');
+const log = [];
+</script>
+<script type="module" src="../resources/empty.js?pipe=trickle(d1)"></script>
+<script type="importmap" onerror="t.done()">
+{
+ "imports": {
+ "../resources/log.js?pipe=sub&name=A": "../resources/log.js?pipe=sub&name=B"
+ }
+}
+</script>
+<script>
+promise_test(() => {
+ return import("../resources/log.js?pipe=sub&name=A")
+ .then(() => assert_array_equals(log, ["log:A"]))
+ },
+ 'After <script type="module"> import maps are not effective');
+</script>
diff --git a/testing/web-platform/tests/import-maps/acquiring/worker-request.html b/testing/web-platform/tests/import-maps/acquiring/worker-request.html
new file mode 100644
index 0000000000..05474ddff2
--- /dev/null
+++ b/testing/web-platform/tests/import-maps/acquiring/worker-request.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+const log = [];
+new Worker('../resources/empty.js?pipe=trickle(d1)', {type: "module"});
+</script>
+<script type="importmap">
+{
+ "imports": {
+ "../resources/log.js?pipe=sub&name=A": "../resources/log.js?pipe=sub&name=B"
+ }
+}
+</script>
+<script>
+promise_test(() => {
+ return import("../resources/log.js?pipe=sub&name=A")
+ .then(() => assert_array_equals(log, ["log:B"]))
+ },
+ 'After module worker creation import maps are still effective');
+</script>
diff --git a/testing/web-platform/tests/import-maps/bare-specifiers.sub.html b/testing/web-platform/tests/import-maps/bare-specifiers.sub.html
new file mode 100644
index 0000000000..d087cb31c4
--- /dev/null
+++ b/testing/web-platform/tests/import-maps/bare-specifiers.sub.html
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helper.js"></script>
+
+<script>
+// "bare/..." (i.e. without leading "./") are bare specifiers
+// (not relative paths).
+const importMap = `
+{
+ "imports": {
+ "bare/bare": "./resources/log.js?pipe=sub&name=bare",
+ "bare/cross-origin-bare": "https://{{domains[www1]}}:{{ports[https][0]}}/import-maps/resources/log.js?pipe=sub&name=cross-origin-bare",
+ "bare/to-data": "data:text/javascript,log.push('dataURL')",
+
+ "bare/to-bare": "bare/bare"
+ }
+}
+`;
+
+const tests = {
+ // Arrays of expected results for:
+ // - <script src type="module">,
+ // - <script src> (classic script),
+ // - static import, and
+ // - dynamic import.
+
+ // Bare to HTTP(S).
+ "bare/bare":
+ [Result.URL, Result.URL, "log:bare", "log:bare"],
+ "bare/cross-origin-bare":
+ [Result.URL, Result.URL, "log:cross-origin-bare", "log:cross-origin-bare"],
+
+ // Bare to data:
+ "bare/to-data":
+ [Result.URL, Result.URL, "dataURL", "dataURL"],
+
+ // Bare to bare mapping is disabled.
+ "bare/to-bare":
+ [Result.URL, Result.URL, Result.PARSE_ERROR, Result.PARSE_ERROR],
+};
+
+doTests(importMap, null, tests);
+</script>
+<body>
diff --git a/testing/web-platform/tests/import-maps/bare/README.md b/testing/web-platform/tests/import-maps/bare/README.md
new file mode 100644
index 0000000000..ce312530ed
--- /dev/null
+++ b/testing/web-platform/tests/import-maps/bare/README.md
@@ -0,0 +1 @@
+This directory contains resources which might get imported, post-mapping. The exact URLs are important, which is why we don't place it under resources/ or similar.
diff --git a/testing/web-platform/tests/import-maps/bare/__dir__.headers b/testing/web-platform/tests/import-maps/bare/__dir__.headers
new file mode 100644
index 0000000000..e7ec0d6699
--- /dev/null
+++ b/testing/web-platform/tests/import-maps/bare/__dir__.headers
@@ -0,0 +1 @@
+Content-Type: text/javascript
diff --git a/testing/web-platform/tests/import-maps/bare/bare b/testing/web-platform/tests/import-maps/bare/bare
new file mode 100644
index 0000000000..1011e866b8
--- /dev/null
+++ b/testing/web-platform/tests/import-maps/bare/bare
@@ -0,0 +1 @@
+log.push("relative:bare/bare");
diff --git a/testing/web-platform/tests/import-maps/bare/cross-origin-bare b/testing/web-platform/tests/import-maps/bare/cross-origin-bare
new file mode 100644
index 0000000000..64851cc8e2
--- /dev/null
+++ b/testing/web-platform/tests/import-maps/bare/cross-origin-bare
@@ -0,0 +1 @@
+log.push("relative:bare/cross-origin-bare");
diff --git a/testing/web-platform/tests/import-maps/bare/to-bare b/testing/web-platform/tests/import-maps/bare/to-bare
new file mode 100644
index 0000000000..bdb3791bc9
--- /dev/null
+++ b/testing/web-platform/tests/import-maps/bare/to-bare
@@ -0,0 +1 @@
+log.push("relative:bare/to-bare");
diff --git a/testing/web-platform/tests/import-maps/bare/to-data b/testing/web-platform/tests/import-maps/bare/to-data
new file mode 100644
index 0000000000..6f25c5af12
--- /dev/null
+++ b/testing/web-platform/tests/import-maps/bare/to-data
@@ -0,0 +1 @@
+log.push("relative:bare/to-data");
diff --git a/testing/web-platform/tests/import-maps/csp/applied-to-target-dynamic.sub.html b/testing/web-platform/tests/import-maps/csp/applied-to-target-dynamic.sub.html
new file mode 100644
index 0000000000..2c73803206
--- /dev/null
+++ b/testing/web-platform/tests/import-maps/csp/applied-to-target-dynamic.sub.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/test-helper.js"></script>
+<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';">
+<script type="importmap">
+{
+ "imports": {
+ "../resources/log.js?pipe=sub&name=A": "https://{{domains[www1]}}:{{ports[https][0]}}/import-maps/resources/log.js?pipe=sub&name=B",
+ "https://{{domains[www1]}}:{{ports[https][0]}}/import-maps/resources/log.js?pipe=sub&name=C": "../resources/log.js?pipe=sub&name=D"
+ }
+}
+</script>
+<script>
+promise_test(t => {
+ return promise_rejects_js(t, TypeError,
+ import('../resources/log.js?pipe=sub&name=A'),
+ 'Dynamic import should fail');
+ }, 'The URL after mapping violates CSP (but not the URL before mapping)');
+
+promise_test(t => {
+ return import('https://{{domains[www1]}}:{{ports[https][0]}}/import-maps/resources/log.js?pipe=sub&name=C')
+ .then(() => assert_array_equals(log, ["log:D"]));
+ }, 'The URL before mapping violates CSP (but not the URL after mapping)');
+</script>
diff --git a/testing/web-platform/tests/import-maps/csp/applied-to-target.sub.html b/testing/web-platform/tests/import-maps/csp/applied-to-target.sub.html
new file mode 100644
index 0000000000..e6bbfecd0d
--- /dev/null
+++ b/testing/web-platform/tests/import-maps/csp/applied-to-target.sub.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/test-helper.js"></script>
+<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';">
+<script type="importmap">
+{
+ "imports": {
+ "../resources/log.js?pipe=sub&name=A": "https://{{domains[www1]}}:{{ports[https][0]}}/import-maps/resources/log.js?pipe=sub&name=B",
+ "https://{{domains[www1]}}:{{ports[https][0]}}/import-maps/resources/log.js?pipe=sub&name=C": "../resources/log.js?pipe=sub&name=D"
+ }
+}
+</script>
+<script type="module">
+import '../resources/log.js?pipe=sub&name=A';
+</script>
+<script type="module">
+test(t => {
+ assert_array_equals(log, []);
+ }, 'The URL after mapping violates CSP (but not the URL before mapping)');
+</script>
+
+<script type="module">
+import 'https://{{domains[www1]}}:{{ports[https][0]}}/import-maps/resources/log.js?pipe=sub&name=C';
+</script>
+<script type="module">
+test(t => {
+ assert_array_equals(log, ["log:D"]);
+ }, 'The URL before mapping violates CSP (but not the URL after mapping)');
+</script>
diff --git a/testing/web-platform/tests/import-maps/csp/hash-failure.html b/testing/web-platform/tests/import-maps/csp/hash-failure.html
new file mode 100644
index 0000000000..4bab1ed917
--- /dev/null
+++ b/testing/web-platform/tests/import-maps/csp/hash-failure.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'sha256-wrong9e+pZbSYIkpB8BIE0Hs7yHajJDiX5mnT/wrong=' 'sha256-RAsyam34o4peVe9sCebtaZWRVhqAhudem+NlcnP2Kp8=';">
+
+<!-- 'sha256-P5xqp9e+pZbSYIkpB8BIE0Hs7yHajJDiX5mnT/1PO1I=' -->
+<script type="importmap">
+{
+ "imports": {
+ "../resources/log.js?pipe=sub&name=A": "../resources/log.js?pipe=sub&name=B"
+ }
+}
+</script>
+
+<!-- 'sha256-RAsyam34o4peVe9sCebtaZWRVhqAhudem+NlcnP2Kp8=' -->
+<script>
+const log = [];
+promise_test(() => {
+ return import("../resources/log.js?pipe=sub&name=A")
+ .then(() => assert_array_equals(log, ["log:A"]))
+ },
+ 'Importmap should not be accepted due to wrong hash');
+</script>
diff --git a/testing/web-platform/tests/import-maps/csp/hash.html b/testing/web-platform/tests/import-maps/csp/hash.html
new file mode 100644
index 0000000000..868c5beb81
--- /dev/null
+++ b/testing/web-platform/tests/import-maps/csp/hash.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'sha256-P5xqp9e+pZbSYIkpB8BIE0Hs7yHajJDiX5mnT/1PO1I=' 'sha256-Ciqph+wQDoB2suzqZVHOD0iw99WqaTUwZXRl7ATzBxc=';">
+
+<!-- 'sha256-P5xqp9e+pZbSYIkpB8BIE0Hs7yHajJDiX5mnT/1PO1I=' -->
+<script type="importmap">
+{
+ "imports": {
+ "../resources/log.js?pipe=sub&name=A": "../resources/log.js?pipe=sub&name=B"
+ }
+}
+</script>
+
+<!-- 'sha256-Ciqph+wQDoB2suzqZVHOD0iw99WqaTUwZXRl7ATzBxc=' -->
+<script>
+const log = [];
+promise_test(() => {
+ return import("../resources/log.js?pipe=sub&name=A")
+ .then(() => assert_array_equals(log, ["log:B"]))
+ },
+ 'Importmap should be accepted due to hash');
+</script>
diff --git a/testing/web-platform/tests/import-maps/csp/nonce-failure.html b/testing/web-platform/tests/import-maps/csp/nonce-failure.html
new file mode 100644
index 0000000000..a1661a432a
--- /dev/null
+++ b/testing/web-platform/tests/import-maps/csp/nonce-failure.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<meta http-equiv="Content-Security-Policy" content="script-src 'nonce-abc';">
+<script type="importmap">
+{
+ "imports": {
+ "../resources/log.js?pipe=sub&name=A": "../resources/log.js?pipe=sub&name=B"
+ }
+}
+</script>
+<script type="importmap" nonce="wrong">
+{
+ "imports": {
+ "../resources/log.js?pipe=sub&name=A": "../resources/log.js?pipe=sub&name=B"
+ }
+}
+</script>
+<script nonce="abc">
+const log = [];
+promise_test(() => {
+ return import("../resources/log.js?pipe=sub&name=A")
+ .then(() => assert_array_equals(log, ["log:A"]))
+ },
+ 'Importmap should be rejected due to nonce');
+</script>
diff --git a/testing/web-platform/tests/import-maps/csp/nonce.html b/testing/web-platform/tests/import-maps/csp/nonce.html
new file mode 100644
index 0000000000..858c572214
--- /dev/null
+++ b/testing/web-platform/tests/import-maps/csp/nonce.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<meta http-equiv="Content-Security-Policy" content="script-src 'nonce-abc';">
+<script type="importmap" nonce="abc">
+{
+ "imports": {
+ "../resources/log.js?pipe=sub&name=A": "../resources/log.js?pipe=sub&name=B"
+ }
+}
+</script>
+<script nonce="abc">
+const log = [];
+promise_test(() => {
+ return import("../resources/log.js?pipe=sub&name=A")
+ .then(() => assert_array_equals(log, ["log:B"]))
+ },
+ 'Importmap should be accepted according to its correct nonce');
+</script>
diff --git a/testing/web-platform/tests/import-maps/csp/unsafe-inline.html b/testing/web-platform/tests/import-maps/csp/unsafe-inline.html
new file mode 100644
index 0000000000..101c33cf84
--- /dev/null
+++ b/testing/web-platform/tests/import-maps/csp/unsafe-inline.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';">
+<script type="importmap">
+{
+ "imports": {
+ "../resources/log.js?pipe=sub&name=A": "../resources/log.js?pipe=sub&name=B"
+ }
+}
+</script>
+<script>
+const log = [];
+promise_test(() => {
+ return import("../resources/log.js?pipe=sub&name=A")
+ .then(() => assert_array_equals(log, ["log:B"]))
+ },
+ 'Importmap should be accepted due to unsafe-inline');
+</script>
diff --git a/testing/web-platform/tests/import-maps/data-driven/README.md b/testing/web-platform/tests/import-maps/data-driven/README.md
new file mode 100644
index 0000000000..abf059e468
--- /dev/null
+++ b/testing/web-platform/tests/import-maps/data-driven/README.md
@@ -0,0 +1,87 @@
+# Data-driven import maps tests
+
+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 other
+implementations that might not run the full WPT suite, e.g. server-side
+JavaScript runtimes or the [JavaScript reference implementation](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 test expectations.
+Test expectations consist of the expected resulting URLs for specifiers.
+
+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).
+- `expectedResults` (object; string to (string or null)): resolution test cases.
+ - The keys are specifiers to be resolved.
+ - The values are expected resolved URLs. If `null`, resolution should fail.
+- `baseURL` (string): the base URL used in [resolving a specifier](https://wicg.github.io/import-maps/#resolve-a-module-specifier) for each specifiers.
+
+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.
+
+## TODO
+
+The `parsing-*.json` files are not currently used by the WPT harness. We should
+convert them to resolution tests.
diff --git a/testing/web-platform/tests/import-maps/data-driven/resolving.html b/testing/web-platform/tests/import-maps/data-driven/resolving.html
new file mode 100644
index 0000000000..bcf3d1de7e
--- /dev/null
+++ b/testing/web-platform/tests/import-maps/data-driven/resolving.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<meta name="variant" content="?data-url-prefix.json">
+<meta name="variant" content="?empty-import-map.json">
+<meta name="variant" content="?overlapping-entries.json">
+<meta name="variant" content="?packages-via-trailing-slashes.json">
+<meta name="variant" content="?resolving-null.json">
+<meta name="variant" content="?scopes-exact-vs-prefix.json">
+<meta name="variant" content="?scopes.json">
+<meta name="variant" content="?tricky-specifiers.json">
+<meta name="variant" content="?url-specifiers-schemes.json">
+<meta name="variant" content="?url-specifiers.json">
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<script type="module">
+import { runTestsFromJSON } from "./resources/test-helper.js";
+
+const filename = location.search.substring(1);
+promise_test(
+ () => runTestsFromJSON('resources/' + filename),
+ "Test helper: fetching and sanity checking test JSON: " + filename);
+</script>
diff --git a/testing/web-platform/tests/import-maps/data-driven/resources/data-url-prefix.json b/testing/web-platform/tests/import-maps/data-driven/resources/data-url-prefix.json
new file mode 100644
index 0000000000..980f6e005f
--- /dev/null
+++ b/testing/web-platform/tests/import-maps/data-driven/resources/data-url-prefix.json
@@ -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: URL prefix",
+ "tests": {
+ "should not resolve since you can't resolve relative to a data: URL": {
+ "expectedResults": {
+ "foo/bar": null
+ }
+ }
+ }
+}
diff --git a/testing/web-platform/tests/import-maps/data-driven/resources/empty-import-map.json b/testing/web-platform/tests/import-maps/data-driven/resources/empty-import-map.json
new file mode 100644
index 0000000000..f488759aa4
--- /dev/null
+++ b/testing/web-platform/tests/import-maps/data-driven/resources/empty-import-map.json
@@ -0,0 +1,61 @@
+{
+ "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"
+ }
+ },
+ "HTTPS scheme absolute URLs": {
+ "expectedResults": {
+ "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///"
+ }
+ },
+ "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
+ }
+ },
+ "non-HTTPS fetch scheme absolute URLs": {
+ "expectedResults": {
+ "about:fetch-scheme": "about:fetch-scheme"
+ }
+ },
+ "non-fetch scheme absolute URLs": {
+ "expectedResults": {
+ "about:fetch-scheme": "about:fetch-scheme",
+ "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/"
+ }
+ }
+ }
+}
diff --git a/testing/web-platform/tests/import-maps/data-driven/resources/overlapping-entries.json b/testing/web-platform/tests/import-maps/data-driven/resources/overlapping-entries.json
new file mode 100644
index 0000000000..2135402545
--- /dev/null
+++ b/testing/web-platform/tests/import-maps/data-driven/resources/overlapping-entries.json
@@ -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"
+ }
+ }
+ }
+}
diff --git a/testing/web-platform/tests/import-maps/data-driven/resources/packages-via-trailing-slashes.json b/testing/web-platform/tests/import-maps/data-driven/resources/packages-via-trailing-slashes.json
new file mode 100644
index 0000000000..03959fafd2
--- /dev/null
+++ b/testing/web-platform/tests/import-maps/data-driven/resources/packages-via-trailing-slashes.json
@@ -0,0 +1,74 @@
+{
+ "importMap": {
+ "imports": {
+ "moment": "/node_modules/moment/src/moment.js",
+ "moment/": "/node_modules/moment/src/",
+ "lodash-dot": "./node_modules/lodash-es/lodash.js",
+ "lodash-dot/": "./node_modules/lodash-es/",
+ "lodash-dotdot": "../node_modules/lodash-es/lodash.js",
+ "lodash-dotdot/": "../node_modules/lodash-es/",
+ "mapped/": "https://example.com/",
+ "mapped/path/": "https://github.com/WICG/import-maps/issues/207/",
+ "mapped/non-ascii-1/": "https://example.com/%E3%81%8D%E3%81%A4%E3%81%AD/",
+ "mapped/non-ascii-2/": "https://example.com/きつね/"
+ }
+ },
+ "importMapBaseURL": "https://example.com/app/index.html",
+ "baseURL": "https://example.com/js/app.mjs",
+ "name": "Package-like scenarios",
+ "link": "https://github.com/WICG/import-maps#packages-via-trailing-slashes",
+ "tests": {
+ "package main modules": {
+ "expectedResults": {
+ "moment": "https://example.com/node_modules/moment/src/moment.js",
+ "lodash-dot": "https://example.com/app/node_modules/lodash-es/lodash.js",
+ "lodash-dotdot": "https://example.com/node_modules/lodash-es/lodash.js"
+ }
+ },
+ "package submodules": {
+ "expectedResults": {
+ "moment/foo": "https://example.com/node_modules/moment/src/foo",
+ "moment/foo?query": "https://example.com/node_modules/moment/src/foo?query",
+ "moment/foo#fragment": "https://example.com/node_modules/moment/src/foo#fragment",
+ "moment/foo?query#fragment": "https://example.com/node_modules/moment/src/foo?query#fragment",
+ "lodash-dot/foo": "https://example.com/app/node_modules/lodash-es/foo",
+ "lodash-dotdot/foo": "https://example.com/node_modules/lodash-es/foo"
+ }
+ },
+ "package names that end in a slash should just pass through": {
+ "expectedResults": {
+ "moment/": "https://example.com/node_modules/moment/src/"
+ }
+ },
+ "package modules that are not declared should fail": {
+ "expectedResults": {
+ "underscore/": null,
+ "underscore/foo": null
+ }
+ },
+ "backtracking via ..": {
+ "expectedResults": {
+ "mapped/path": "https://example.com/path",
+ "mapped/path/": "https://github.com/WICG/import-maps/issues/207/",
+ "mapped/path/..": null,
+ "mapped/path/../path/": null,
+ "mapped/path/../207": null,
+ "mapped/path/../207/": "https://github.com/WICG/import-maps/issues/207/",
+ "mapped/path//": null,
+ "mapped/path/WICG/import-maps/issues/207/": "https://github.com/WICG/import-maps/issues/207/WICG/import-maps/issues/207/",
+ "mapped/path//WICG/import-maps/issues/207/": "https://github.com/WICG/import-maps/issues/207/",
+ "mapped/path/../backtrack": null,
+ "mapped/path/../../backtrack": null,
+ "mapped/path/../../../backtrack": null,
+ "moment/../backtrack": null,
+ "moment/..": null,
+ "mapped/non-ascii-1/": "https://example.com/%E3%81%8D%E3%81%A4%E3%81%AD/",
+ "mapped/non-ascii-1/../%E3%81%8D%E3%81%A4%E3%81%AD/": "https://example.com/%E3%81%8D%E3%81%A4%E3%81%AD/",
+ "mapped/non-ascii-1/../きつね/": "https://example.com/%E3%81%8D%E3%81%A4%E3%81%AD/",
+ "mapped/non-ascii-2/": "https://example.com/%E3%81%8D%E3%81%A4%E3%81%AD/",
+ "mapped/non-ascii-2/../%E3%81%8D%E3%81%A4%E3%81%AD/": "https://example.com/%E3%81%8D%E3%81%A4%E3%81%AD/",
+ "mapped/non-ascii-2/../きつね/": "https://example.com/%E3%81%8D%E3%81%A4%E3%81%AD/"
+ }
+ }
+ }
+}
diff --git a/testing/web-platform/tests/import-maps/data-driven/resources/parsing-addresses-absolute.json b/testing/web-platform/tests/import-maps/data-driven/resources/parsing-addresses-absolute.json
new file mode 100644
index 0000000000..b4004395e7
--- /dev/null
+++ b/testing/web-platform/tests/import-maps/data-driven/resources/parsing-addresses-absolute.json
@@ -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": {}
+ }
+ }
+ }
+}
diff --git a/testing/web-platform/tests/import-maps/data-driven/resources/parsing-addresses-invalid.json b/testing/web-platform/tests/import-maps/data-driven/resources/parsing-addresses-invalid.json
new file mode 100644
index 0000000000..4e5f182df6
--- /dev/null
+++ b/testing/web-platform/tests/import-maps/data-driven/resources/parsing-addresses-invalid.json
@@ -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": {}
+ }
+ }
+ }
+}
diff --git a/testing/web-platform/tests/import-maps/data-driven/resources/parsing-addresses.json b/testing/web-platform/tests/import-maps/data-driven/resources/parsing-addresses.json
new file mode 100644
index 0000000000..fe92709565
--- /dev/null
+++ b/testing/web-platform/tests/import-maps/data-driven/resources/parsing-addresses.json
@@ -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": {}
+ }
+ }
+ }
+}
diff --git a/testing/web-platform/tests/import-maps/data-driven/resources/parsing-invalid-json.json b/testing/web-platform/tests/import-maps/data-driven/resources/parsing-invalid-json.json
new file mode 100644
index 0000000000..1bd1c94e89
--- /dev/null
+++ b/testing/web-platform/tests/import-maps/data-driven/resources/parsing-invalid-json.json
@@ -0,0 +1,6 @@
+{
+ "name": "Invalid JSON",
+ "importMapBaseURL": "https://base.example/",
+ "importMap": "{imports: {}}",
+ "expectedParsedImportMap": null
+}
diff --git a/testing/web-platform/tests/import-maps/data-driven/resources/parsing-schema-normalization.json b/testing/web-platform/tests/import-maps/data-driven/resources/parsing-schema-normalization.json
new file mode 100644
index 0000000000..a330bb8799
--- /dev/null
+++ b/testing/web-platform/tests/import-maps/data-driven/resources/parsing-schema-normalization.json
@@ -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": {}
+ }
+ }
+ }
+}
diff --git a/testing/web-platform/tests/import-maps/data-driven/resources/parsing-schema-scope.json b/testing/web-platform/tests/import-maps/data-driven/resources/parsing-schema-scope.json
new file mode 100644
index 0000000000..04d09395c3
--- /dev/null
+++ b/testing/web-platform/tests/import-maps/data-driven/resources/parsing-schema-scope.json
@@ -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/": []
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/testing/web-platform/tests/import-maps/data-driven/resources/parsing-schema-specifier-map.json b/testing/web-platform/tests/import-maps/data-driven/resources/parsing-schema-specifier-map.json
new file mode 100644
index 0000000000..7d7d4be2fe
--- /dev/null
+++ b/testing/web-platform/tests/import-maps/data-driven/resources/parsing-schema-specifier-map.json
@@ -0,0 +1,44 @@
+{
+ "name": "Mismatching the specifier map schema",
+ "importMapBaseURL": "https://base.example/",
+ "tests": {
+ "should ignore entries where the address is not a string": {
+ "importMap": {
+ "imports": {
+ "null": null,
+ "boolean": true,
+ "number": 1,
+ "object": {},
+ "array": [],
+ "array2": [
+ "https://example.com/"
+ ],
+ "string": "https://example.com/"
+ }
+ },
+ "expectedParsedImportMap": {
+ "imports": {
+ "null": null,
+ "boolean": null,
+ "number": null,
+ "object": null,
+ "array": null,
+ "array2": null,
+ "string": "https://example.com/"
+ },
+ "scopes": {}
+ }
+ },
+ "should ignore entries where the specifier key is an empty string": {
+ "importMap": {
+ "imports": {
+ "": "https://example.com/"
+ }
+ },
+ "expectedParsedImportMap": {
+ "imports": {},
+ "scopes": {}
+ }
+ }
+ }
+}
diff --git a/testing/web-platform/tests/import-maps/data-driven/resources/parsing-schema-toplevel.json b/testing/web-platform/tests/import-maps/data-driven/resources/parsing-schema-toplevel.json
new file mode 100644
index 0000000000..278cad2295
--- /dev/null
+++ b/testing/web-platform/tests/import-maps/data-driven/resources/parsing-schema-toplevel.json
@@ -0,0 +1,97 @@
+{
+ "name": "Mismatching the top-level schema",
+ "importMapBaseURL": "https://base.example/",
+ "tests": {
+ "should throw for top-level non-objects": {
+ "expectedParsedImportMap": null,
+ "tests": {
+ "null": {
+ "importMap": null
+ },
+ "boolean": {
+ "importMap": true
+ },
+ "number": {
+ "importMap": 1
+ },
+ "string": {
+ "importMap": "foo"
+ },
+ "array": {
+ "importMap": []
+ }
+ }
+ },
+ "should throw if imports is a non-object": {
+ "expectedParsedImportMap": null,
+ "tests": {
+ "null": {
+ "importMap": {
+ "imports": null
+ }
+ },
+ "boolean": {
+ "importMap": {
+ "imports": true
+ }
+ },
+ "number": {
+ "importMap": {
+ "imports": 1
+ }
+ },
+ "string": {
+ "importMap": {
+ "imports": "foo"
+ }
+ },
+ "array": {
+ "importMap": {
+ "imports": []
+ }
+ }
+ }
+ },
+ "should throw if scopes is a non-object": {
+ "expectedParsedImportMap": null,
+ "tests": {
+ "null": {
+ "importMap": {
+ "scopes": null
+ }
+ },
+ "boolean": {
+ "importMap": {
+ "scopes": true
+ }
+ },
+ "number": {
+ "importMap": {
+ "scopes": 1
+ }
+ },
+ "string": {
+ "importMap": {
+ "scopes": "foo"
+ }
+ },
+ "array": {
+ "importMap": {
+ "scopes": []
+ }
+ }
+ }
+ },
+ "should ignore unspecified top-level entries": {
+ "importMap": {
+ "imports": {},
+ "new-feature": {},
+ "scops": {}
+ },
+ "expectedParsedImportMap": {
+ "imports": {},
+ "scopes": {}
+ }
+ }
+ }
+}
diff --git a/testing/web-platform/tests/import-maps/data-driven/resources/parsing-scope-keys.json b/testing/web-platform/tests/import-maps/data-driven/resources/parsing-scope-keys.json
new file mode 100644
index 0000000000..4b2f1eeada
--- /dev/null
+++ b/testing/web-platform/tests/import-maps/data-driven/resources/parsing-scope-keys.json
@@ -0,0 +1,191 @@
+{
+ "importMapBaseURL": "https://base.example/path1/path2/path3",
+ "tests": {
+ "Relative URL scope keys should work with no prefix": {
+ "importMap": {
+ "scopes": {
+ "foo": {}
+ }
+ },
+ "expectedParsedImportMap": {
+ "imports": {},
+ "scopes": {
+ "https://base.example/path1/path2/foo": {}
+ }
+ }
+ },
+ "Relative URL scope keys should work with ./, ../, and / prefixes": {
+ "importMap": {
+ "scopes": {
+ "./foo": {},
+ "../foo": {},
+ "/foo": {}
+ }
+ },
+ "expectedParsedImportMap": {
+ "imports": {},
+ "scopes": {
+ "https://base.example/path1/path2/foo": {},
+ "https://base.example/path1/foo": {},
+ "https://base.example/foo": {}
+ }
+ }
+ },
+ "Absolute URL scope keys should ignore relative URL scope keys when the base URL is a data: URL": {
+ "importMap": {
+ "scopes": {
+ "./foo": {},
+ "../foo": {},
+ "/foo": {}
+ }
+ },
+ "importMapBaseURL": "data:text/html,test",
+ "expectedParsedImportMap": {
+ "imports": {},
+ "scopes": {}
+ }
+ },
+ "Relative URL scope keys should work with ./, ../, or / with no suffix": {
+ "importMap": {
+ "scopes": {
+ "./": {},
+ "../": {},
+ "/": {}
+ }
+ },
+ "expectedParsedImportMap": {
+ "imports": {},
+ "scopes": {
+ "https://base.example/path1/path2/": {},
+ "https://base.example/path1/": {},
+ "https://base.example/": {}
+ }
+ }
+ },
+ "Relative URL scope keys should work with /s, ?s, and #s": {
+ "importMap": {
+ "scopes": {
+ "foo/bar?baz#qux": {}
+ }
+ },
+ "expectedParsedImportMap": {
+ "imports": {},
+ "scopes": {
+ "https://base.example/path1/path2/foo/bar?baz#qux": {}
+ }
+ }
+ },
+ "Relative URL scope keys should work with an empty string scope key": {
+ "importMap": {
+ "scopes": {
+ "": {}
+ }
+ },
+ "expectedParsedImportMap": {
+ "imports": {},
+ "scopes": {
+ "https://base.example/path1/path2/path3": {}
+ }
+ }
+ },
+ "Relative URL scope keys should work with / suffixes": {
+ "importMap": {
+ "scopes": {
+ "foo/": {},
+ "./foo/": {},
+ "../foo/": {},
+ "/foo/": {},
+ "/foo//": {}
+ }
+ },
+ "expectedParsedImportMap": {
+ "imports": {},
+ "scopes": {
+ "https://base.example/path1/path2/foo/": {},
+ "https://base.example/path1/foo/": {},
+ "https://base.example/foo/": {},
+ "https://base.example/foo//": {}
+ }
+ }
+ },
+ "Relative URL scope keys should deduplicate based on URL parsing rules": {
+ "importMap": {
+ "scopes": {
+ "foo/\\": {
+ "1": "./a"
+ },
+ "foo//": {
+ "2": "./b"
+ },
+ "foo\\\\": {
+ "3": "./c"
+ }
+ }
+ },
+ "expectedParsedImportMap": {
+ "imports": {},
+ "scopes": {
+ "https://base.example/path1/path2/foo//": {
+ "3": "https://base.example/path1/path2/c"
+ }
+ }
+ }
+ },
+ "Absolute URL scope keys should accept all absolute URL scope keys, with or without fetch schemes": {
+ "importMap": {
+ "scopes": {
+ "about:good": {},
+ "blob:good": {},
+ "data:good": {},
+ "file:///good": {},
+ "filesystem:http://example.com/good/": {},
+ "http://good/": {},
+ "https://good/": {},
+ "ftp://good/": {},
+ "import:bad": {},
+ "mailto:bad": {},
+ "javascript:bad": {},
+ "wss:ba": {}
+ }
+ },
+ "expectedParsedImportMap": {
+ "imports": {},
+ "scopes": {
+ "about:good": {},
+ "blob:good": {},
+ "data:good": {},
+ "file:///good": {},
+ "filesystem:http://example.com/good/": {},
+ "http://good/": {},
+ "https://good/": {},
+ "ftp://good/": {},
+ "import:bad": {},
+ "mailto:bad": {},
+ "javascript:bad": {},
+ "wss://ba/": {}
+ }
+ }
+ },
+ "Absolute URL scope keys should parse absolute URL scope keys, ignoring unparseable ones": {
+ "importMap": {
+ "scopes": {
+ "https://example.com:demo": {},
+ "http://[www.example.com]/": {},
+ "https:example.org": {},
+ "https://///example.com///": {},
+ "https://example.net": {},
+ "https://ex%41mple.com/foo/": {}
+ }
+ },
+ "expectedParsedImportMap": {
+ "imports": {},
+ "scopes": {
+ "https://base.example/path1/path2/example.org": {},
+ "https://example.com///": {},
+ "https://example.net/": {},
+ "https://example.com/foo/": {}
+ }
+ }
+ }
+ }
+}
diff --git a/testing/web-platform/tests/import-maps/data-driven/resources/parsing-specifier-keys.json b/testing/web-platform/tests/import-maps/data-driven/resources/parsing-specifier-keys.json
new file mode 100644
index 0000000000..b2d9cf47fa
--- /dev/null
+++ b/testing/web-platform/tests/import-maps/data-driven/resources/parsing-specifier-keys.json
@@ -0,0 +1,209 @@
+{
+ "importMapBaseURL": "https://base.example/path1/path2/path3",
+ "tests": {
+ "Relative URL specifier keys should absolutize strings prefixed with ./, ../, or / into the corresponding URLs": {
+ "importMap": {
+ "imports": {
+ "./foo": "/dotslash",
+ "../foo": "/dotdotslash",
+ "/foo": "/slash"
+ }
+ },
+ "expectedParsedImportMap": {
+ "imports": {
+ "https://base.example/path1/path2/foo": "https://base.example/dotslash",
+ "https://base.example/path1/foo": "https://base.example/dotdotslash",
+ "https://base.example/foo": "https://base.example/slash"
+ },
+ "scopes": {}
+ }
+ },
+ "Relative URL specifier keys should not absolutize strings prefixed with ./, ../, or / with a data: URL base": {
+ "importMap": {
+ "imports": {
+ "./foo": "https://example.com/dotslash",
+ "../foo": "https://example.com/dotdotslash",
+ "/foo": "https://example.com/slash"
+ }
+ },
+ "importMapBaseURL": "data:text/html,",
+ "expectedParsedImportMap": {
+ "imports": {
+ "./foo": "https://example.com/dotslash",
+ "../foo": "https://example.com/dotdotslash",
+ "/foo": "https://example.com/slash"
+ },
+ "scopes": {}
+ }
+ },
+ "Relative URL specifier keys should absolutize the literal strings ./, ../, or / with no suffix": {
+ "importMap": {
+ "imports": {
+ "./": "/dotslash/",
+ "../": "/dotdotslash/",
+ "/": "/slash/"
+ }
+ },
+ "expectedParsedImportMap": {
+ "imports": {
+ "https://base.example/path1/path2/": "https://base.example/dotslash/",
+ "https://base.example/path1/": "https://base.example/dotdotslash/",
+ "https://base.example/": "https://base.example/slash/"
+ },
+ "scopes": {}
+ }
+ },
+ "Relative URL specifier keys should work with /s, ?s, and #s": {
+ "importMap": {
+ "imports": {
+ "./foo/bar?baz#qux": "/foo"
+ }
+ },
+ "expectedParsedImportMap": {
+ "imports": {
+ "https://base.example/path1/path2/foo/bar?baz#qux": "https://base.example/foo"
+ },
+ "scopes": {}
+ }
+ },
+ "Relative URL specifier keys should ignore an empty string key": {
+ "importMap": {
+ "imports": {
+ "": "/foo"
+ }
+ },
+ "expectedParsedImportMap": {
+ "imports": {},
+ "scopes": {}
+ }
+ },
+ "Relative URL specifier keys should treat percent-encoded variants of ./, ../, or / as bare specifiers": {
+ "importMap": {
+ "imports": {
+ "%2E/": "/dotSlash1/",
+ "%2E%2E/": "/dotDotSlash1/",
+ ".%2F": "/dotSlash2",
+ "..%2F": "/dotDotSlash2",
+ "%2F": "/slash2",
+ "%2E%2F": "/dotSlash3",
+ "%2E%2E%2F": "/dotDotSlash3"
+ }
+ },
+ "expectedParsedImportMap": {
+ "imports": {
+ "%2E/": "https://base.example/dotSlash1/",
+ "%2E%2E/": "https://base.example/dotDotSlash1/",
+ ".%2F": "https://base.example/dotSlash2",
+ "..%2F": "https://base.example/dotDotSlash2",
+ "%2F": "https://base.example/slash2",
+ "%2E%2F": "https://base.example/dotSlash3",
+ "%2E%2E%2F": "https://base.example/dotDotSlash3"
+ },
+ "scopes": {}
+ }
+ },
+ "Relative URL specifier keys should deduplicate based on URL parsing rules": {
+ "importMap": {
+ "imports": {
+ "./foo/\\": "/foo1",
+ "./foo//": "/foo2",
+ "./foo\\\\": "/foo3"
+ }
+ },
+ "expectedParsedImportMap": {
+ "imports": {
+ "https://base.example/path1/path2/foo//": "https://base.example/foo3"
+ },
+ "scopes": {}
+ }
+ },
+ "Absolute URL specifier keys should accept all absolute URL specifier keys, with or without fetch schemes": {
+ "importMap": {
+ "imports": {
+ "about:good": "/about",
+ "blob:good": "/blob",
+ "data:good": "/data",
+ "file:///good": "/file",
+ "filesystem:http://example.com/good/": "/filesystem/",
+ "http://good/": "/http/",
+ "https://good/": "/https/",
+ "ftp://good/": "/ftp/",
+ "import:bad": "/import",
+ "mailto:bad": "/mailto",
+ "javascript:bad": "/javascript",
+ "wss:bad": "/wss"
+ }
+ },
+ "expectedParsedImportMap": {
+ "imports": {
+ "about:good": "https://base.example/about",
+ "blob:good": "https://base.example/blob",
+ "data:good": "https://base.example/data",
+ "file:///good": "https://base.example/file",
+ "filesystem:http://example.com/good/": "https://base.example/filesystem/",
+ "http://good/": "https://base.example/http/",
+ "https://good/": "https://base.example/https/",
+ "ftp://good/": "https://base.example/ftp/",
+ "import:bad": "https://base.example/import",
+ "mailto:bad": "https://base.example/mailto",
+ "javascript:bad": "https://base.example/javascript",
+ "wss://bad/": "https://base.example/wss"
+ },
+ "scopes": {}
+ }
+ },
+ "Absolute URL specifier keys should parse absolute URLs, treating unparseable ones as bare specifiers": {
+ "importMap": {
+ "imports": {
+ "https://example.com:demo": "/unparseable2",
+ "http://[www.example.com]/": "/unparseable3/",
+ "https:example.org": "/invalidButParseable1/",
+ "https://///example.com///": "/invalidButParseable2/",
+ "https://example.net": "/prettyNormal/",
+ "https://ex%41mple.com/": "/percentDecoding/"
+ }
+ },
+ "expectedParsedImportMap": {
+ "imports": {
+ "https://example.com:demo": "https://base.example/unparseable2",
+ "http://[www.example.com]/": "https://base.example/unparseable3/",
+ "https://example.org/": "https://base.example/invalidButParseable1/",
+ "https://example.com///": "https://base.example/invalidButParseable2/",
+ "https://example.net/": "https://base.example/prettyNormal/",
+ "https://example.com/": "https://base.example/percentDecoding/"
+ },
+ "scopes": {}
+ }
+ },
+ "Specifier keys should be sort correctly (issue #181) - Test #1": {
+ "importMap": {
+ "imports": {
+ "https://example.com/aaa": "https://example.com/aaa",
+ "https://example.com/a": "https://example.com/a"
+ }
+ },
+ "expectedParsedImportMap": {
+ "imports": {
+ "https://example.com/aaa": "https://example.com/aaa",
+ "https://example.com/a": "https://example.com/a"
+ },
+ "scopes": {}
+ }
+ },
+ "Specifier keys should be sort correctly (issue #181) - Test #2": {
+ "importMap": {
+ "imports": {
+ "https://example.com/a": "https://example.com/a",
+ "https://example.com/aaa": "https://example.com/aaa"
+ }
+ },
+ "expectedParsedImportMap": {
+ "imports": {
+ "https://example.com/aaa": "https://example.com/aaa",
+ "https://example.com/a": "https://example.com/a"
+ },
+ "scopes": {}
+ }
+ }
+ }
+}
diff --git a/testing/web-platform/tests/import-maps/data-driven/resources/parsing-trailing-slashes.json b/testing/web-platform/tests/import-maps/data-driven/resources/parsing-trailing-slashes.json
new file mode 100644
index 0000000000..89c454fc9f
--- /dev/null
+++ b/testing/web-platform/tests/import-maps/data-driven/resources/parsing-trailing-slashes.json
@@ -0,0 +1,15 @@
+{
+ "name": "Failing addresses: mismatched trailing slashes",
+ "importMap": {
+ "imports": {
+ "trailer/": "/notrailer"
+ }
+ },
+ "importMapBaseURL": "https://base.example/path1/path2/path3",
+ "expectedParsedImportMap": {
+ "imports": {
+ "trailer/": null
+ },
+ "scopes": {}
+ }
+}
diff --git a/testing/web-platform/tests/import-maps/data-driven/resources/resolving-null.json b/testing/web-platform/tests/import-maps/data-driven/resources/resolving-null.json
new file mode 100644
index 0000000000..77563155d1
--- /dev/null
+++ b/testing/web-platform/tests/import-maps/data-driven/resources/resolving-null.json
@@ -0,0 +1,82 @@
+{
+ "importMapBaseURL": "https://example.com/app/index.html",
+ "baseURL": "https://example.com/js/app.mjs",
+ "name": "Entries with errors shouldn't allow fallback",
+ "tests": {
+ "No fallback to less-specific prefixes": {
+ "importMap": {
+ "imports": {
+ "null/": "/1/",
+ "null/b/": null,
+ "null/b/c/": "/1/2/",
+ "invalid-url/": "/1/",
+ "invalid-url/b/": "https://:invalid-url:/",
+ "invalid-url/b/c/": "/1/2/",
+ "without-trailing-slashes/": "/1/",
+ "without-trailing-slashes/b/": "/x",
+ "without-trailing-slashes/b/c/": "/1/2/",
+ "prefix-resolution-error/": "/1/",
+ "prefix-resolution-error/b/": "data:text/javascript,/",
+ "prefix-resolution-error/b/c/": "/1/2/"
+ }
+ },
+ "expectedResults": {
+ "null/x": "https://example.com/1/x",
+ "null/b/x": null,
+ "null/b/c/x": "https://example.com/1/2/x",
+ "invalid-url/x": "https://example.com/1/x",
+ "invalid-url/b/x": null,
+ "invalid-url/b/c/x": "https://example.com/1/2/x",
+ "without-trailing-slashes/x": "https://example.com/1/x",
+ "without-trailing-slashes/b/x": null,
+ "without-trailing-slashes/b/c/x": "https://example.com/1/2/x",
+ "prefix-resolution-error/x": "https://example.com/1/x",
+ "prefix-resolution-error/b/x": null,
+ "prefix-resolution-error/b/c/x": "https://example.com/1/2/x"
+ }
+ },
+ "No fallback to less-specific scopes": {
+ "importMap": {
+ "imports": {
+ "null": "https://example.com/a",
+ "invalid-url": "https://example.com/b",
+ "without-trailing-slashes/": "https://example.com/c/",
+ "prefix-resolution-error/": "https://example.com/d/"
+ },
+ "scopes": {
+ "/js/": {
+ "null": null,
+ "invalid-url": "https://:invalid-url:/",
+ "without-trailing-slashes/": "/x",
+ "prefix-resolution-error/": "data:text/javascript,/"
+ }
+ }
+ },
+ "expectedResults": {
+ "null": null,
+ "invalid-url": null,
+ "without-trailing-slashes/x": null,
+ "prefix-resolution-error/x": null
+ }
+ },
+ "No fallback to absolute URL parsing": {
+ "importMap": {
+ "imports": {},
+ "scopes": {
+ "/js/": {
+ "https://example.com/null": null,
+ "https://example.com/invalid-url": "https://:invalid-url:/",
+ "https://example.com/without-trailing-slashes/": "/x",
+ "https://example.com/prefix-resolution-error/": "data:text/javascript,/"
+ }
+ }
+ },
+ "expectedResults": {
+ "https://example.com/null": null,
+ "https://example.com/invalid-url": null,
+ "https://example.com/without-trailing-slashes/x": null,
+ "https://example.com/prefix-resolution-error/x": null
+ }
+ }
+ }
+}
diff --git a/testing/web-platform/tests/import-maps/data-driven/resources/scopes-exact-vs-prefix.json b/testing/web-platform/tests/import-maps/data-driven/resources/scopes-exact-vs-prefix.json
new file mode 100644
index 0000000000..3d9d50349f
--- /dev/null
+++ b/testing/web-platform/tests/import-maps/data-driven/resources/scopes-exact-vs-prefix.json
@@ -0,0 +1,134 @@
+{
+ "name": "Exact vs. prefix based matching",
+ "details": "Scopes are matched with base URLs that are exactly the same or subpaths under the scopes with trailing shashes",
+ "link": "https://wicg.github.io/import-maps/#resolve-a-module-specifier Step 8.1",
+ "tests": {
+ "Scope without trailing slash only": {
+ "importMap": {
+ "scopes": {
+ "/js": {
+ "moment": "/only-triggered-by-exact/moment",
+ "moment/": "/only-triggered-by-exact/moment/"
+ }
+ }
+ },
+ "importMapBaseURL": "https://example.com/app/index.html",
+ "tests": {
+ "Non-trailing-slash base URL (exact match)": {
+ "baseURL": "https://example.com/js",
+ "expectedResults": {
+ "moment": "https://example.com/only-triggered-by-exact/moment",
+ "moment/foo": "https://example.com/only-triggered-by-exact/moment/foo"
+ }
+ },
+ "Trailing-slash base URL (fail)": {
+ "baseURL": "https://example.com/js/",
+ "expectedResults": {
+ "moment": null,
+ "moment/foo": null
+ }
+ },
+ "Subpath base URL (fail)": {
+ "baseURL": "https://example.com/js/app.mjs",
+ "expectedResults": {
+ "moment": null,
+ "moment/foo": null
+ }
+ },
+ "Non-subpath base URL (fail)": {
+ "baseURL": "https://example.com/jsiscool",
+ "expectedResults": {
+ "moment": null,
+ "moment/foo": null
+ }
+ }
+ }
+ },
+ "Scope with trailing slash only": {
+ "importMap": {
+ "scopes": {
+ "/js/": {
+ "moment": "/triggered-by-any-subpath/moment",
+ "moment/": "/triggered-by-any-subpath/moment/"
+ }
+ }
+ },
+ "importMapBaseURL": "https://example.com/app/index.html",
+ "tests": {
+ "Non-trailing-slash base URL (fail)": {
+ "baseURL": "https://example.com/js",
+ "expectedResults": {
+ "moment": null,
+ "moment/foo": null
+ }
+ },
+ "Trailing-slash base URL (exact match)": {
+ "baseURL": "https://example.com/js/",
+ "expectedResults": {
+ "moment": "https://example.com/triggered-by-any-subpath/moment",
+ "moment/foo": "https://example.com/triggered-by-any-subpath/moment/foo"
+ }
+ },
+ "Subpath base URL (prefix match)": {
+ "baseURL": "https://example.com/js/app.mjs",
+ "expectedResults": {
+ "moment": "https://example.com/triggered-by-any-subpath/moment",
+ "moment/foo": "https://example.com/triggered-by-any-subpath/moment/foo"
+ }
+ },
+ "Non-subpath base URL (fail)": {
+ "baseURL": "https://example.com/jsiscool",
+ "expectedResults": {
+ "moment": null,
+ "moment/foo": null
+ }
+ }
+ }
+ },
+ "Scopes with and without trailing slash": {
+ "importMap": {
+ "scopes": {
+ "/js": {
+ "moment": "/only-triggered-by-exact/moment",
+ "moment/": "/only-triggered-by-exact/moment/"
+ },
+ "/js/": {
+ "moment": "/triggered-by-any-subpath/moment",
+ "moment/": "/triggered-by-any-subpath/moment/"
+ }
+ }
+ },
+ "importMapBaseURL": "https://example.com/app/index.html",
+ "tests": {
+ "Non-trailing-slash base URL (exact match)": {
+ "baseURL": "https://example.com/js",
+ "expectedResults": {
+ "moment": "https://example.com/only-triggered-by-exact/moment",
+ "moment/foo": "https://example.com/only-triggered-by-exact/moment/foo"
+ }
+ },
+ "Trailing-slash base URL (exact match)": {
+ "baseURL": "https://example.com/js/",
+ "expectedResults": {
+ "moment": "https://example.com/triggered-by-any-subpath/moment",
+ "moment/foo": "https://example.com/triggered-by-any-subpath/moment/foo"
+ }
+ },
+ "Subpath base URL (prefix match)": {
+ "baseURL": "https://example.com/js/app.mjs",
+ "expectedResults": {
+ "moment": "https://example.com/triggered-by-any-subpath/moment",
+ "moment/foo": "https://example.com/triggered-by-any-subpath/moment/foo"
+ }
+ },
+ "Non-subpath base URL (fail)": {
+ "baseURL": "https://example.com/jsiscool",
+ "expectedResults": {
+ "moment": null,
+ "moment/foo": null
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/testing/web-platform/tests/import-maps/data-driven/resources/scopes.json b/testing/web-platform/tests/import-maps/data-driven/resources/scopes.json
new file mode 100644
index 0000000000..c266e4c6c1
--- /dev/null
+++ b/testing/web-platform/tests/import-maps/data-driven/resources/scopes.json
@@ -0,0 +1,171 @@
+{
+ "importMapBaseURL": "https://example.com/app/index.html",
+ "tests": {
+ "Fallback to toplevel and between scopes": {
+ "importMap": {
+ "imports": {
+ "a": "/a-1.mjs",
+ "b": "/b-1.mjs",
+ "c": "/c-1.mjs",
+ "d": "/d-1.mjs"
+ },
+ "scopes": {
+ "/scope2/": {
+ "a": "/a-2.mjs",
+ "d": "/d-2.mjs"
+ },
+ "/scope2/scope3/": {
+ "b": "/b-3.mjs",
+ "d": "/d-3.mjs"
+ }
+ }
+ },
+ "tests": {
+ "should fall back to `imports` when no scopes match": {
+ "baseURL": "https://example.com/scope1/foo.mjs",
+ "expectedResults": {
+ "a": "https://example.com/a-1.mjs",
+ "b": "https://example.com/b-1.mjs",
+ "c": "https://example.com/c-1.mjs",
+ "d": "https://example.com/d-1.mjs"
+ }
+ },
+ "should use a direct scope override": {
+ "baseURL": "https://example.com/scope2/foo.mjs",
+ "expectedResults": {
+ "a": "https://example.com/a-2.mjs",
+ "b": "https://example.com/b-1.mjs",
+ "c": "https://example.com/c-1.mjs",
+ "d": "https://example.com/d-2.mjs"
+ }
+ },
+ "should use an indirect scope override": {
+ "baseURL": "https://example.com/scope2/scope3/foo.mjs",
+ "expectedResults": {
+ "a": "https://example.com/a-2.mjs",
+ "b": "https://example.com/b-3.mjs",
+ "c": "https://example.com/c-1.mjs",
+ "d": "https://example.com/d-3.mjs"
+ }
+ }
+ }
+ },
+ "Relative URL scope keys": {
+ "importMap": {
+ "imports": {
+ "a": "/a-1.mjs",
+ "b": "/b-1.mjs",
+ "c": "/c-1.mjs"
+ },
+ "scopes": {
+ "": {
+ "a": "/a-empty-string.mjs"
+ },
+ "./": {
+ "b": "/b-dot-slash.mjs"
+ },
+ "../": {
+ "c": "/c-dot-dot-slash.mjs"
+ }
+ }
+ },
+ "tests": {
+ "An empty string scope is a scope with import map base URL": {
+ "baseURL": "https://example.com/app/index.html",
+ "expectedResults": {
+ "a": "https://example.com/a-empty-string.mjs",
+ "b": "https://example.com/b-dot-slash.mjs",
+ "c": "https://example.com/c-dot-dot-slash.mjs"
+ }
+ },
+ "'./' scope is a scope with import map base URL's directory": {
+ "baseURL": "https://example.com/app/foo.mjs",
+ "expectedResults": {
+ "a": "https://example.com/a-1.mjs",
+ "b": "https://example.com/b-dot-slash.mjs",
+ "c": "https://example.com/c-dot-dot-slash.mjs"
+ }
+ },
+ "'../' scope is a scope with import map base URL's parent directory": {
+ "baseURL": "https://example.com/foo.mjs",
+ "expectedResults": {
+ "a": "https://example.com/a-1.mjs",
+ "b": "https://example.com/b-1.mjs",
+ "c": "https://example.com/c-dot-dot-slash.mjs"
+ }
+ }
+ }
+ },
+ "Package-like scenarios": {
+ "importMap": {
+ "imports": {
+ "moment": "/node_modules/moment/src/moment.js",
+ "moment/": "/node_modules/moment/src/",
+ "lodash-dot": "./node_modules/lodash-es/lodash.js",
+ "lodash-dot/": "./node_modules/lodash-es/",
+ "lodash-dotdot": "../node_modules/lodash-es/lodash.js",
+ "lodash-dotdot/": "../node_modules/lodash-es/"
+ },
+ "scopes": {
+ "/": {
+ "moment": "/node_modules_3/moment/src/moment.js",
+ "vue": "/node_modules_3/vue/dist/vue.runtime.esm.js"
+ },
+ "/js/": {
+ "lodash-dot": "./node_modules_2/lodash-es/lodash.js",
+ "lodash-dot/": "./node_modules_2/lodash-es/",
+ "lodash-dotdot": "../node_modules_2/lodash-es/lodash.js",
+ "lodash-dotdot/": "../node_modules_2/lodash-es/"
+ }
+ }
+ },
+ "tests": {
+ "Base URLs inside the scope should use the scope if the scope has matching keys": {
+ "baseURL": "https://example.com/js/app.mjs",
+ "expectedResults": {
+ "lodash-dot": "https://example.com/app/node_modules_2/lodash-es/lodash.js",
+ "lodash-dot/foo": "https://example.com/app/node_modules_2/lodash-es/foo",
+ "lodash-dotdot": "https://example.com/node_modules_2/lodash-es/lodash.js",
+ "lodash-dotdot/foo": "https://example.com/node_modules_2/lodash-es/foo"
+ }
+ },
+ "Base URLs inside the scope fallback to less specific scope": {
+ "baseURL": "https://example.com/js/app.mjs",
+ "expectedResults": {
+ "moment": "https://example.com/node_modules_3/moment/src/moment.js",
+ "vue": "https://example.com/node_modules_3/vue/dist/vue.runtime.esm.js"
+ }
+ },
+ "Base URLs inside the scope fallback to toplevel": {
+ "baseURL": "https://example.com/js/app.mjs",
+ "expectedResults": {
+ "moment/foo": "https://example.com/node_modules/moment/src/foo"
+ }
+ },
+ "Base URLs outside a scope shouldn't use the scope even if the scope has matching keys": {
+ "baseURL": "https://example.com/app.mjs",
+ "expectedResults": {
+ "lodash-dot": "https://example.com/app/node_modules/lodash-es/lodash.js",
+ "lodash-dotdot": "https://example.com/node_modules/lodash-es/lodash.js",
+ "lodash-dot/foo": "https://example.com/app/node_modules/lodash-es/foo",
+ "lodash-dotdot/foo": "https://example.com/node_modules/lodash-es/foo"
+ }
+ },
+ "Fallback to toplevel or not, depending on trailing slash match": {
+ "baseURL": "https://example.com/app.mjs",
+ "expectedResults": {
+ "moment": "https://example.com/node_modules_3/moment/src/moment.js",
+ "moment/foo": "https://example.com/node_modules/moment/src/foo"
+ }
+ },
+ "should still fail for package-like specifiers that are not declared": {
+ "baseURL": "https://example.com/js/app.mjs",
+ "expectedResults": {
+ "underscore/": null,
+ "underscore/foo": null
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/testing/web-platform/tests/import-maps/data-driven/resources/test-helper-iframe.js b/testing/web-platform/tests/import-maps/data-driven/resources/test-helper-iframe.js
new file mode 100644
index 0000000000..3f38a5f0fa
--- /dev/null
+++ b/testing/web-platform/tests/import-maps/data-driven/resources/test-helper-iframe.js
@@ -0,0 +1,46 @@
+// Handle errors around fetching, parsing and registering import maps.
+window.onScriptError = event => {
+ window.registrationResult = {type: 'FetchError', error: event.error};
+ return false;
+};
+window.windowErrorHandler = event => {
+ window.registrationResult = {type: 'ParseError', error: event.error};
+ return false;
+};
+window.addEventListener('error', window.windowErrorHandler);
+
+// Handle specifier resolution requests from the parent frame.
+// For failures, we post error names and messages instead of error
+// objects themselves and re-create error objects later, to avoid
+// issues around serializing error objects which is a quite new feature.
+window.addEventListener('message', event => {
+ if (event.data.action !== 'resolve') {
+ parent.postMessage({
+ type: 'Failure',
+ result: 'Error',
+ message: 'Invalid Action: ' + event.data.action}, '*');
+ return;
+ }
+
+ // To respond to a resolution request, we:
+ // 1. Save the specifier to resolve into a global.
+ // 2. Update the document's base URL to the requested base URL.
+ // 3. Create a new inline script, parsed with that base URL, which
+ // resolves the saved specifier using import.meta.resolve(), and
+ // sents the result to the parent window.
+ window.specifierToResolve = event.data.specifier;
+ document.querySelector('base').href = event.data.baseURL;
+
+ const inlineScript = document.createElement('script');
+ inlineScript.type = 'module';
+ inlineScript.textContent = `
+ try {
+ const result = import.meta.resolve(window.specifierToResolve);
+ parent.postMessage({type: 'ResolutionSuccess', result}, '*');
+ } catch (e) {
+ parent.postMessage(
+ {type: 'Failure', result: e.name, message: e.message}, '*');
+ }
+ `;
+ document.body.append(inlineScript);
+});
diff --git a/testing/web-platform/tests/import-maps/data-driven/resources/test-helper.js b/testing/web-platform/tests/import-maps/data-driven/resources/test-helper.js
new file mode 100644
index 0000000000..d34869b013
--- /dev/null
+++ b/testing/web-platform/tests/import-maps/data-driven/resources/test-helper.js
@@ -0,0 +1,142 @@
+setup({allow_uncaught_exception : true});
+
+// Creates a new Document (via <iframe>) and add an inline import map.
+function createTestIframe(importMap, importMapBaseURL) {
+ return new Promise(resolve => {
+ const iframe = document.createElement('iframe');
+
+ window.addEventListener('message', event => {
+ // Parsing result is saved here and checked later, rather than
+ // rejecting the promise on errors.
+ iframe.parseImportMapResult = event.data.type;
+ resolve(iframe);
+ },
+ {once: true});
+
+ const testHTML = createTestHTML(importMap, importMapBaseURL);
+
+ if (new URL(importMapBaseURL).protocol === 'data:') {
+ iframe.src = 'data:text/html;base64,' + btoa(testHTML);
+ } else {
+ iframe.src = '/common/blank.html';
+ iframe.addEventListener('load', () => {
+ iframe.contentDocument.write(testHTML);
+ iframe.contentDocument.close();
+ }, {once: true});
+ }
+ document.body.appendChild(iframe);
+ });
+}
+
+function createTestHTML(importMap, importMapBaseURL) {
+ return `
+ <!DOCTYPE html>
+ <script src="${location.origin}/import-maps/data-driven/resources/test-helper-iframe.js"></script>
+
+ <base href="${importMapBaseURL}">
+ <script type="importmap" onerror="onScriptError(event)">
+ ${JSON.stringify(importMap)}
+ </script>
+
+ <script type="module">
+ if (!window.registrationResult) {
+ window.registrationResult = {type: 'Success'};
+ }
+ window.removeEventListener('error', window.windowErrorHandler);
+ parent.postMessage(window.registrationResult, '*');
+ </script>
+ `;
+}
+
+// Returns a promise that is resolved with the resulting URL, or rejected if
+// the resolution fails.
+function resolve(specifier, baseURL, iframe) {
+ return new Promise((resolve, reject) => {
+ window.addEventListener('message', event => {
+ if (event.data.type === 'ResolutionSuccess') {
+ resolve(event.data.result);
+ } else if (event.data.type === 'Failure') {
+ if (event.data.result === 'TypeError') {
+ reject(new TypeError(event.data.message));
+ } else {
+ reject(new Error(event.data.message));
+ }
+ } else {
+ assert_unreached('Invalid message: ' + event.data.type);
+ }
+ },
+ {once: true});
+
+ iframe.contentWindow.postMessage(
+ {action: 'resolve', specifier, 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.hasOwnProperty('importMap')) {
+ assert_own_property(j, 'importMap');
+ assert_own_property(j, 'importMapBaseURL');
+ j.iframe = await createTestIframe(j.importMap, j.importMapBaseURL);
+ delete j.importMap;
+ delete j.importMapBaseURL;
+ }
+
+ assert_no_extra_properties(
+ j,
+ ['expectedResults', 'expectedParsedImportMap',
+ 'baseURL', 'name', 'iframe',
+ '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({}, j, tests[testName]);
+ await runTests(k);
+ }
+ } else {
+ // Leaf node.
+ for (const key of ['iframe', 'name', 'expectedResults']) {
+ assert_own_property(j, key, j.name);
+ }
+
+ assert_equals(
+ j.iframe.parseImportMapResult,
+ 'Success',
+ 'Import map registration should be successful for resolution tests');
+ for (const [specifier, expected] of Object.entries(j.expectedResults)) {
+ promise_test(async t => {
+ if (expected === null) {
+ return promise_rejects_js(t, TypeError, resolve(specifier, j.baseURL, j.iframe));
+ } else {
+ assert_equals(await resolve(specifier, j.baseURL, j.iframe), expected);
+ }
+ },
+ j.name + ': ' + specifier);
+ }
+ }
+}
+
+export async function runTestsFromJSON(jsonURL) {
+ const response = await fetch(jsonURL);
+ const json = await response.json();
+ await runTests(json);
+}
diff --git a/testing/web-platform/tests/import-maps/data-driven/resources/tricky-specifiers.json b/testing/web-platform/tests/import-maps/data-driven/resources/tricky-specifiers.json
new file mode 100644
index 0000000000..998fe7fb67
--- /dev/null
+++ b/testing/web-platform/tests/import-maps/data-driven/resources/tricky-specifiers.json
@@ -0,0 +1,71 @@
+{
+ "importMap": {
+ "imports": {
+ "package/withslash": "/node_modules/package-with-slash/index.mjs",
+ "not-a-package": "/lib/not-a-package.mjs",
+ "only-slash/": "/lib/only-slash/",
+ ".": "/lib/dot.mjs",
+ "..": "/lib/dotdot.mjs",
+ "..\\": "/lib/dotdotbackslash.mjs",
+ "%2E": "/lib/percent2e.mjs",
+ "%2F": "/lib/percent2f.mjs",
+ "https://map.example/%E3%81%8D%E3%81%A4%E3%81%AD/": "/a/",
+ "https://map.example/きつね/fox/": "/b/",
+ "%E3%81%8D%E3%81%A4%E3%81%AD/": "/c/",
+ "きつね/fox/": "/d/"
+ }
+ },
+ "importMapBaseURL": "https://example.com/app/index.html",
+ "baseURL": "https://example.com/js/app.mjs",
+ "name": "Tricky specifiers",
+ "tests": {
+ "explicitly-mapped specifiers that happen to have a slash": {
+ "expectedResults": {
+ "package/withslash": "https://example.com/node_modules/package-with-slash/index.mjs"
+ }
+ },
+ "specifier with punctuation": {
+ "expectedResults": {
+ ".": "https://example.com/lib/dot.mjs",
+ "..": "https://example.com/lib/dotdot.mjs",
+ "..\\": "https://example.com/lib/dotdotbackslash.mjs",
+ "%2E": "https://example.com/lib/percent2e.mjs",
+ "%2F": "https://example.com/lib/percent2f.mjs"
+ }
+ },
+ "submodule of something not declared with a trailing slash should fail": {
+ "expectedResults": {
+ "not-a-package/foo": null
+ }
+ },
+ "module for which only a trailing-slash version is present should fail": {
+ "expectedResults": {
+ "only-slash": null
+ }
+ },
+ "URL-like specifiers are normalized": {
+ "expectedResults": {
+ "https://map.example/%E3%81%8D%E3%81%A4%E3%81%AD/": "https://example.com/a/",
+ "https://map.example/%E3%81%8D%E3%81%A4%E3%81%AD/bar": "https://example.com/a/bar",
+ "https://map.example/%E3%81%8D%E3%81%A4%E3%81%AD/fox/": "https://example.com/b/",
+ "https://map.example/%E3%81%8D%E3%81%A4%E3%81%AD/fox/bar": "https://example.com/b/bar",
+ "https://map.example/きつね/": "https://example.com/a/",
+ "https://map.example/きつね/bar": "https://example.com/a/bar",
+ "https://map.example/きつね/fox/": "https://example.com/b/",
+ "https://map.example/きつね/fox/bar": "https://example.com/b/bar"
+ }
+ },
+ "Bare specifiers are not normalized": {
+ "expectedResults": {
+ "%E3%81%8D%E3%81%A4%E3%81%AD/": "https://example.com/c/",
+ "%E3%81%8D%E3%81%A4%E3%81%AD/bar": "https://example.com/c/bar",
+ "%E3%81%8D%E3%81%A4%E3%81%AD/fox/": "https://example.com/c/fox/",
+ "%E3%81%8D%E3%81%A4%E3%81%AD/fox/bar": "https://example.com/c/fox/bar",
+ "きつね/": null,
+ "きつね/bar": null,
+ "きつね/fox/": "https://example.com/d/",
+ "きつね/fox/bar": "https://example.com/d/bar"
+ }
+ }
+ }
+}
diff --git a/testing/web-platform/tests/import-maps/data-driven/resources/url-specifiers-schemes.json b/testing/web-platform/tests/import-maps/data-driven/resources/url-specifiers-schemes.json
new file mode 100644
index 0000000000..58a97642c2
--- /dev/null
+++ b/testing/web-platform/tests/import-maps/data-driven/resources/url-specifiers-schemes.json
@@ -0,0 +1,45 @@
+{
+ "importMap": {
+ "imports": {
+ "data:text/": "/lib/test-data/",
+ "about:text/": "/lib/test-about/",
+ "blob:text/": "/lib/test-blob/",
+ "blah:text/": "/lib/test-blah/",
+ "http:text/": "/lib/test-http/",
+ "https:text/": "/lib/test-https/",
+ "file:text/": "/lib/test-file/",
+ "ftp:text/": "/lib/test-ftp/",
+ "ws:text/": "/lib/test-ws/",
+ "wss:text/": "/lib/test-wss/"
+ }
+ },
+ "importMapBaseURL": "https://example.com/app/index.html",
+ "baseURL": "https://example.com/js/app.mjs",
+ "name": "URL-like specifiers",
+ "tests": {
+ "Non-special vs. special schemes": {
+ "expectedResults": {
+ "data:text/javascript,console.log('foo')": "data:text/javascript,console.log('foo')",
+ "data:text/": "https://example.com/lib/test-data/",
+ "about:text/foo": "about:text/foo",
+ "about:text/": "https://example.com/lib/test-about/",
+ "blob:text/foo": "blob:text/foo",
+ "blob:text/": "https://example.com/lib/test-blob/",
+ "blah:text/foo": "blah:text/foo",
+ "blah:text/": "https://example.com/lib/test-blah/",
+ "http:text/foo": "https://example.com/lib/test-http/foo",
+ "http:text/": "https://example.com/lib/test-http/",
+ "https:text/foo": "https://example.com/lib/test-https/foo",
+ "https:text/": "https://example.com/lib/test-https/",
+ "ftp:text/foo": "https://example.com/lib/test-ftp/foo",
+ "ftp:text/": "https://example.com/lib/test-ftp/",
+ "file:text/foo": "https://example.com/lib/test-file/foo",
+ "file:text/": "https://example.com/lib/test-file/",
+ "ws:text/foo": "https://example.com/lib/test-ws/foo",
+ "ws:text/": "https://example.com/lib/test-ws/",
+ "wss:text/foo": "https://example.com/lib/test-wss/foo",
+ "wss:text/": "https://example.com/lib/test-wss/"
+ }
+ }
+ }
+}
diff --git a/testing/web-platform/tests/import-maps/data-driven/resources/url-specifiers.json b/testing/web-platform/tests/import-maps/data-driven/resources/url-specifiers.json
new file mode 100644
index 0000000000..6fcf7f4663
--- /dev/null
+++ b/testing/web-platform/tests/import-maps/data-driven/resources/url-specifiers.json
@@ -0,0 +1,68 @@
+{
+ "importMap": {
+ "imports": {
+ "/lib/foo.mjs": "./more/bar.mjs",
+ "./dotrelative/foo.mjs": "/lib/dot.mjs",
+ "../dotdotrelative/foo.mjs": "/lib/dotdot.mjs",
+ "/": "/lib/slash-only/",
+ "./": "/lib/dotslash-only/",
+ "/test/": "/lib/url-trailing-slash/",
+ "./test/": "/lib/url-trailing-slash-dot/",
+ "/test": "/lib/test1.mjs",
+ "../test": "/lib/test2.mjs"
+ }
+ },
+ "importMapBaseURL": "https://example.com/app/index.html",
+ "baseURL": "https://example.com/js/app.mjs",
+ "name": "URL-like specifiers",
+ "tests": {
+ "Ordinary URL-like specifiers": {
+ "expectedResults": {
+ "https://example.com/lib/foo.mjs": "https://example.com/app/more/bar.mjs",
+ "https://///example.com/lib/foo.mjs": "https://example.com/app/more/bar.mjs",
+ "/lib/foo.mjs": "https://example.com/app/more/bar.mjs",
+ "https://example.com/app/dotrelative/foo.mjs": "https://example.com/lib/dot.mjs",
+ "../app/dotrelative/foo.mjs": "https://example.com/lib/dot.mjs",
+ "https://example.com/dotdotrelative/foo.mjs": "https://example.com/lib/dotdot.mjs",
+ "../dotdotrelative/foo.mjs": "https://example.com/lib/dotdot.mjs"
+ }
+ },
+ "Import map entries just composed from / and .": {
+ "expectedResults": {
+ "https://example.com/": "https://example.com/lib/slash-only/",
+ "/": "https://example.com/lib/slash-only/",
+ "../": "https://example.com/lib/slash-only/",
+ "https://example.com/app/": "https://example.com/lib/dotslash-only/",
+ "/app/": "https://example.com/lib/dotslash-only/",
+ "../app/": "https://example.com/lib/dotslash-only/"
+ }
+ },
+ "prefix-matched by keys with trailing slashes": {
+ "expectedResults": {
+ "/test/foo.mjs": "https://example.com/lib/url-trailing-slash/foo.mjs",
+ "https://example.com/app/test/foo.mjs": "https://example.com/lib/url-trailing-slash-dot/foo.mjs"
+ }
+ },
+ "should use the last entry's address when URL-like specifiers parse to the same absolute URL": {
+ "expectedResults": {
+ "/test": "https://example.com/lib/test2.mjs"
+ }
+ },
+ "backtracking (relative URLs)": {
+ "expectedResults": {
+ "/test/..": "https://example.com/lib/slash-only/",
+ "/test/../backtrack": "https://example.com/lib/slash-only/backtrack",
+ "/test/../../backtrack": "https://example.com/lib/slash-only/backtrack",
+ "/test/../../../backtrack": "https://example.com/lib/slash-only/backtrack"
+ }
+ },
+ "backtracking (absolute URLs)": {
+ "expectedResults": {
+ "https://example.com/test/..": "https://example.com/lib/slash-only/",
+ "https://example.com/test/../backtrack": "https://example.com/lib/slash-only/backtrack",
+ "https://example.com/test/../../backtrack": "https://example.com/lib/slash-only/backtrack",
+ "https://example.com/test/../../../backtrack": "https://example.com/lib/slash-only/backtrack"
+ }
+ }
+ }
+}
diff --git a/testing/web-platform/tests/import-maps/data-driven/tools/format_json.py b/testing/web-platform/tests/import-maps/data-driven/tools/format_json.py
new file mode 100644
index 0000000000..6386d418de
--- /dev/null
+++ b/testing/web-platform/tests/import-maps/data-driven/tools/format_json.py
@@ -0,0 +1,27 @@
+import collections
+import json
+import sys
+import traceback
+"""
+Simple JSON formatter, to be used for JSON files under resources/.
+
+Usage:
+$ python tools/format_json.py resources/*.json
+"""
+
+
+def main():
+ for filename in sys.argv[1:]:
+ print(filename)
+ try:
+ spec = json.load(
+ open(filename, u'r'), object_pairs_hook=collections.OrderedDict)
+ with open(filename, u'w') as f:
+ f.write(json.dumps(spec, indent=2, separators=(u',', u': ')))
+ f.write(u'\n')
+ except:
+ traceback.print_exc()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/testing/web-platform/tests/import-maps/data-url-specifiers.sub.html b/testing/web-platform/tests/import-maps/data-url-specifiers.sub.html
new file mode 100644
index 0000000000..a6fa12c126
--- /dev/null
+++ b/testing/web-platform/tests/import-maps/data-url-specifiers.sub.html
@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helper.js"></script>
+
+<script>
+// "bare/..." (i.e. without leading "./") are bare specifiers
+// (not relative paths).
+const importMap = `
+{
+ "imports": {
+ "bare": "./resources/log.js?pipe=sub&name=bare",
+
+ "data:text/javascript,log.push('data:foo')": "./resources/log.js?pipe=sub&name=foo",
+ "data:text/javascript,log.push('data:cross-origin-foo')": "https://{{domains[www1]}}:{{ports[https][0]}}/import-maps/resources/log.js?pipe=sub&name=cross-origin-foo",
+ "data:text/javascript,log.push('data:to-data')": "data:text/javascript,log.push('dataURL')",
+
+ "data:text/javascript,log.push('data:to-bare')": "bare"
+ }
+}
+`;
+
+const tests = {
+ // Arrays of expected results for:
+ // - <script src type="module">,
+ // - <script src> (classic script),
+ // - static import, and
+ // - dynamic import.
+
+ // data: to HTTP(S).
+ "data:text/javascript,log.push('data:foo')":
+ [Result.URL, Result.URL, "log:foo", "log:foo"],
+ "data:text/javascript,log.push('data:cross-origin-foo')":
+ [Result.URL, Result.URL, "log:cross-origin-foo", "log:cross-origin-foo"],
+
+ // data: to data:
+ "data:text/javascript,log.push('data:to-data')":
+ [Result.URL, Result.URL, "dataURL", "dataURL"],
+
+ // data: to bare mapping is disabled.
+ "data:text/javascript,log.push('data:to-bare')":
+ [Result.URL, Result.URL, Result.PARSE_ERROR, Result.PARSE_ERROR],
+};
+
+doTests(importMap, null, tests);
+</script>
+<body>
diff --git a/testing/web-platform/tests/import-maps/http-url-like-specifiers.sub.html b/testing/web-platform/tests/import-maps/http-url-like-specifiers.sub.html
new file mode 100644
index 0000000000..ba5182354c
--- /dev/null
+++ b/testing/web-platform/tests/import-maps/http-url-like-specifiers.sub.html
@@ -0,0 +1,47 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helper.js"></script>
+
+<script>
+// "bare/..." (i.e. without leading "./") are bare specifiers
+// (not relative paths).
+const importMap = `
+{
+ "imports": {
+ "bare": "./resources/log.js?pipe=sub&name=bare",
+
+ "./resources/log.js?pipe=sub&name=foo": "./resources/log.js?pipe=sub&name=bar",
+ "./resources/log.js?pipe=sub&name=cross-origin-foo": "https://{{domains[www1]}}:{{ports[https][0]}}/import-maps/resources/log.js?pipe=sub&name=cross-origin-bar",
+ "./resources/log.js?pipe=sub&name=to-data": "data:text/javascript,log.push('dataURL')",
+
+ "./resources/log.js?pipe=sub&name=to-bare": "bare"
+ }
+}
+`;
+
+const tests = {
+ // Arrays of expected results for:
+ // - <script src type="module">,
+ // - <script src> (classic script),
+ // - static import, and
+ // - dynamic import.
+
+ // HTTP(S) to HTTP(S).
+ "{{location[server]}}/import-maps/resources/log.js?pipe=sub&name=foo":
+ [Result.URL, Result.URL, "log:bar", "log:bar"],
+ "{{location[server]}}/import-maps/resources/log.js?pipe=sub&name=cross-origin-foo":
+ [Result.URL, Result.URL, "log:cross-origin-bar", "log:cross-origin-bar"],
+
+ // HTTP(S) to data:
+ "{{location[server]}}/import-maps/resources/log.js?pipe=sub&name=to-data":
+ [Result.URL, Result.URL, "dataURL", "dataURL"],
+
+ // HTTP(S) to bare mapping is disabled.
+ "{{location[server]}}/import-maps/resources/log.js?pipe=sub&name=to-bare":
+ [Result.URL, Result.URL, Result.PARSE_ERROR, Result.PARSE_ERROR],
+};
+
+doTests(importMap, null, tests);
+</script>
+<body>
diff --git a/testing/web-platform/tests/import-maps/import-maps-base-url.sub.html b/testing/web-platform/tests/import-maps/import-maps-base-url.sub.html
new file mode 100644
index 0000000000..5d44eae4b2
--- /dev/null
+++ b/testing/web-platform/tests/import-maps/import-maps-base-url.sub.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helper.js"></script>
+
+<script>
+
+// baseURL will be used to create a <base> element, which will change the
+// baseURL of the import map.
+const baseURL = "http://{{host}}:{{ports[http][0]}}/import-maps/resources/";
+const importMap = `
+{
+ "imports": {
+ "bare/bare": "./log.js?pipe=sub&name=bare"
+ }
+}
+`;
+
+promise_setup(function () {
+ return new Promise((resolve) => {
+ window.addEventListener("load", async () => {
+ await testStaticImport(importMap, baseURL, "bare/bare", "log:bare");
+ await testDynamicImport(importMap, baseURL, "bare/bare", "log:bare", "module");
+ await testDynamicImport(importMap, baseURL, "bare/bare", "log:bare", "text/javascript");
+
+ await testStaticImportInjectBase(importMap, baseURL, "bare/bare", "log:bare");
+ await testDynamicImportInjectBase(importMap, baseURL, "bare/bare", "log:bare", "module");
+ await testDynamicImportInjectBase(importMap, baseURL, "bare/bare", "log:bare", "text/javascript");
+ done();
+ resolve();
+ });
+ });
+}, { explicit_done: true });
+
+
+</script>
+<body>
diff --git a/testing/web-platform/tests/import-maps/module-map-key.html b/testing/web-platform/tests/import-maps/module-map-key.html
new file mode 100644
index 0000000000..3bf15934aa
--- /dev/null
+++ b/testing/web-platform/tests/import-maps/module-map-key.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script type="importmap">
+{
+ "imports": {
+ "./resources/log.js?pipe=sub&name=A": "./resources/log.js?pipe=sub&name=B"
+ }
+}
+</script>
+<script>
+const log = [];
+
+promise_test(() => {
+ return import("./resources/log.js?pipe=sub&name=A")
+ .then(() => import("./resources/log.js?pipe=sub&name=B"))
+ .then(() => assert_array_equals(log, ["log:B"]))
+ },
+ "Module map's key is the URL after import map resolution");
+</script>
diff --git a/testing/web-platform/tests/import-maps/multiple-import-maps/basic.html b/testing/web-platform/tests/import-maps/multiple-import-maps/basic.html
new file mode 100644
index 0000000000..9ab03ddc30
--- /dev/null
+++ b/testing/web-platform/tests/import-maps/multiple-import-maps/basic.html
@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+const log = [];
+</script>
+<script type="importmap" onerror="log.push('onerror 1')">
+{
+ "imports": {
+ "../resources/log.js?pipe=sub&name=A1": "../resources/log.js?pipe=sub&name=B1",
+ "../resources/log.js?pipe=sub&name=A2": "../resources/log.js?pipe=sub&name=B2"
+ }
+}
+</script>
+<script type="importmap" onerror="log.push('onerror 2')">
+{
+ "imports": {
+ "../resources/log.js?pipe=sub&name=A1": "../resources/log.js?pipe=sub&name=C1",
+ "../resources/log.js?pipe=sub&name=A3": "../resources/log.js?pipe=sub&name=C3"
+ }
+}
+</script>
+<script>
+// Currently the spec doesn't allow multiple import maps, by setting acquiring
+// import maps to false on preparing the first import map.
+promise_test(() => {
+ return import("../resources/log.js?pipe=sub&name=A1")
+ .then(() => import("../resources/log.js?pipe=sub&name=A2"))
+ .then(() => import("../resources/log.js?pipe=sub&name=A3"))
+ .then(() => assert_array_equals(
+ log,
+ ["onerror 2", "log:B1", "log:B2", "log:A3"]))
+ },
+ "Second import map should be rejected");
+</script>
diff --git a/testing/web-platform/tests/import-maps/multiple-import-maps/with-errors.html b/testing/web-platform/tests/import-maps/multiple-import-maps/with-errors.html
new file mode 100644
index 0000000000..a93ecaaeff
--- /dev/null
+++ b/testing/web-platform/tests/import-maps/multiple-import-maps/with-errors.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+setup({allow_uncaught_exception : true});
+
+const log = [];
+</script>
+<script type="importmap" onerror="log.push('onerror 1')">
+Parse Error
+</script>
+<script type="importmap" onerror="log.push('onerror 2')">
+{
+ "imports": {
+ "../resources/log.js?pipe=sub&name=A": "../resources/log.js?pipe=sub&name=C"
+ }
+}
+</script>
+<script>
+// Currently the spec doesn't allow multiple import maps, by setting acquiring
+// import maps to false on preparing the first import map.
+// Even the first import map has errors and thus Document's import map is not
+// updated, the second import map is still rejected at preparationg.
+promise_test(() => {
+ return import("../resources/log.js?pipe=sub&name=A")
+ .then(() => assert_array_equals(
+ log,
+ ["onerror 2", "log:A"]))
+ },
+ "Second import map should be rejected after an import map with errors");
+</script>
diff --git a/testing/web-platform/tests/import-maps/not-as-classic-script.html b/testing/web-platform/tests/import-maps/not-as-classic-script.html
new file mode 100644
index 0000000000..5f97394e44
--- /dev/null
+++ b/testing/web-platform/tests/import-maps/not-as-classic-script.html
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<script>
+setup({allow_uncaught_exception : true});
+
+const t_parse = async_test("Import maps shouldn't be parsed as scripts");
+const t_evaluate = async_test("Import maps shouldn't be executed as scripts");
+const t_external = async_test(
+ "External import maps shouldn't be executed as scripts");
+
+const errorHandler = event => {
+ event.preventDefault();
+ t_parse.unreached_func("An import map is parsed as a classic script")();
+};
+
+window.addEventListener("error", errorHandler, {once: true});
+</script>
+
+<!-- This import map causes a parse error when parsed as a classic script. -->
+<script type="importmap">
+{
+ "imports": {
+ }
+}
+</script>
+
+<script>
+// Remove error handler, because the following import map can causes parse
+// error.
+window.removeEventListener("error", errorHandler);
+</script>
+
+<script type="importmap">
+t_evaluate.unreached_func("An import map is evaluated")();
+</script>
+
+<script type="importmap" src="data:text/javascript,t_external.unreached_func('An external import map is evaluated')();"></script>
+
+<script>
+t_parse.done();
+t_evaluate.done();
+t_external.done();
+</script>
diff --git a/testing/web-platform/tests/import-maps/resources/empty.js b/testing/web-platform/tests/import-maps/resources/empty.js
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/import-maps/resources/empty.js
diff --git a/testing/web-platform/tests/import-maps/resources/inject-base.js b/testing/web-platform/tests/import-maps/resources/inject-base.js
new file mode 100644
index 0000000000..4d469aec89
--- /dev/null
+++ b/testing/web-platform/tests/import-maps/resources/inject-base.js
@@ -0,0 +1,3 @@
+const el = document.createElement("base");
+el.href = "{{GET[baseurl]}}";
+document.currentScript.after(el);
diff --git a/testing/web-platform/tests/import-maps/resources/log.js b/testing/web-platform/tests/import-maps/resources/log.js
new file mode 100644
index 0000000000..a024a29bf2
--- /dev/null
+++ b/testing/web-platform/tests/import-maps/resources/log.js
@@ -0,0 +1 @@
+log.push("log:{{GET[name]}}");
diff --git a/testing/web-platform/tests/import-maps/resources/log.js.headers b/testing/web-platform/tests/import-maps/resources/log.js.headers
new file mode 100644
index 0000000000..cb762eff80
--- /dev/null
+++ b/testing/web-platform/tests/import-maps/resources/log.js.headers
@@ -0,0 +1 @@
+Access-Control-Allow-Origin: *
diff --git a/testing/web-platform/tests/import-maps/resources/test-helper.js b/testing/web-platform/tests/import-maps/resources/test-helper.js
new file mode 100644
index 0000000000..f12e84c685
--- /dev/null
+++ b/testing/web-platform/tests/import-maps/resources/test-helper.js
@@ -0,0 +1,246 @@
+let log = [];
+
+function expect_log(test, expected_log) {
+ test.step_func_done(() => {
+ const actual_log = log;
+ log = [];
+ assert_array_equals(actual_log, expected_log, 'fallback log');
+ })();
+}
+
+// Results of resolving a specifier using import maps.
+const Result = {
+ // A failure considered as a fetch error in a module script tree.
+ // <script>'s error event is fired.
+ FETCH_ERROR: "fetch_error",
+
+ // A failure considered as a parse error in a module script tree.
+ // Window's error event is fired.
+ PARSE_ERROR: "parse_error",
+
+ // The specifier is considered as a relative or absolute URL.
+ // Specifier Expected log
+ // ------------------------- ----------------------
+ // ...?name=foo log:foo
+ // data:...log('foo') foo
+ // Others, e.g. bare/bare relative:bare/bare
+ // ------------------------- ----------------------
+ // (The last case assumes a file `bare/bare` that logs `relative:bare/bare`
+ // exists)
+ URL: "URL",
+};
+
+const Handler = {
+ // Handlers for <script> element cases.
+ // Note that on a parse error both WindowErrorEvent and ScriptLoadEvent are
+ // called.
+ ScriptLoadEvent: "<script> element's load event handler",
+ ScriptErrorEvent: "<script> element's error event handler",
+ WindowErrorEvent: "window's error event handler",
+
+ // Handlers for dynamic imports.
+ DynamicImportResolve: "dynamic import resolve",
+ DynamicImportReject: "dynamic import reject",
+};
+
+// Returns a map with Handler.* as the keys.
+function getHandlers(t, specifier, expected) {
+ let handlers = {};
+ handlers[Handler.ScriptLoadEvent] = t.unreached_func("Shouldn't load");
+ handlers[Handler.ScriptErrorEvent] =
+ t.unreached_func("script's error event shouldn't be fired");
+ handlers[Handler.WindowErrorEvent] =
+ t.unreached_func("window's error event shouldn't be fired");
+ handlers[Handler.DynamicImportResolve] =
+ t.unreached_func("dynamic import promise shouldn't be resolved");
+ handlers[Handler.DynamicImportReject] =
+ t.unreached_func("dynamic import promise shouldn't be rejected");
+
+ if (expected === Result.FETCH_ERROR) {
+ handlers[Handler.ScriptErrorEvent] = () => expect_log(t, []);
+ handlers[Handler.DynamicImportReject] = () => expect_log(t, []);
+ } else if (expected === Result.PARSE_ERROR) {
+ let error_occurred = false;
+ handlers[Handler.WindowErrorEvent] = () => { error_occurred = true; };
+ handlers[Handler.ScriptLoadEvent] = t.step_func(() => {
+ // Even if a parse error occurs, load event is fired (after
+ // window.onerror is called), so trigger the load handler only if
+ // there was no previous window.onerror call.
+ assert_true(error_occurred, "window.onerror should be fired");
+ expect_log(t, []);
+ });
+ handlers[Handler.DynamicImportReject] = t.step_func(() => {
+ assert_false(error_occurred,
+ "window.onerror shouldn't be fired for dynamic imports");
+ expect_log(t, []);
+ });
+ } else {
+ let expected_log;
+ if (expected === Result.URL) {
+ const match_data_url = specifier.match(/data:.*log\.push\('(.*)'\)/);
+ const match_log_js = specifier.match(/name=(.*)/);
+ if (match_data_url) {
+ expected_log = [match_data_url[1]];
+ } else if (match_log_js) {
+ expected_log = ["log:" + match_log_js[1]];
+ } else {
+ expected_log = ["relative:" + specifier];
+ }
+ } else {
+ expected_log = [expected];
+ }
+ handlers[Handler.ScriptLoadEvent] = () => expect_log(t, expected_log);
+ handlers[Handler.DynamicImportResolve] = () => expect_log(t, expected_log);
+ }
+ return handlers;
+}
+
+// Creates an <iframe> and run a test inside the <iframe>
+// to separate the module maps and import maps in each test.
+function testInIframe(importMapString, importMapBaseURL, testScript) {
+ const iframe = document.createElement('iframe');
+ document.body.appendChild(iframe);
+ if (!importMapBaseURL) {
+ importMapBaseURL = document.baseURI;
+ }
+ let content = `
+ <script src="/resources/testharness.js"></script>
+ <script src="/import-maps/resources/test-helper.js"></script>
+ <base href="${importMapBaseURL}">
+ <script type="importmap">${importMapString}</script>
+ <body>
+ <script>
+ setup({ allow_uncaught_exception: true });
+ ${testScript}
+ </sc` + `ript>
+ `;
+ iframe.contentDocument.write(content);
+ iframe.contentDocument.close();
+ return fetch_tests_from_window(iframe.contentWindow);
+}
+
+function testScriptElement(importMapString, importMapBaseURL, specifier, expected, type) {
+ return testInIframe(importMapString, importMapBaseURL, `
+ const t = async_test("${specifier}: <script src type=${type}>");
+ const handlers = getHandlers(t, "${specifier}", "${expected}");
+ const script = document.createElement("script");
+ script.setAttribute("type", "${type}");
+ script.setAttribute("src", "${specifier}");
+ script.addEventListener("load", handlers[Handler.ScriptLoadEvent]);
+ script.addEventListener("error", handlers[Handler.ScriptErrorEvent]);
+ window.addEventListener("error", handlers[Handler.WindowErrorEvent]);
+ document.body.appendChild(script);
+ `);
+}
+
+function testStaticImport(importMapString, importMapBaseURL, specifier, expected) {
+ return testInIframe(importMapString, importMapBaseURL, `
+ const t = async_test("${specifier}: static import");
+ const handlers = getHandlers(t, "${specifier}", "${expected}");
+ const script = document.createElement("script");
+ script.setAttribute("type", "module");
+ script.setAttribute("src",
+ "/import-maps/static-import.py?url=" +
+ encodeURIComponent("${specifier}"));
+ script.addEventListener("load", handlers[Handler.ScriptLoadEvent]);
+ script.addEventListener("error", handlers[Handler.ScriptErrorEvent]);
+ window.addEventListener("error", handlers[Handler.WindowErrorEvent]);
+ document.body.appendChild(script);
+ `);
+}
+
+function testDynamicImport(importMapString, importMapBaseURL, specifier, expected, type) {
+ return testInIframe(importMapString, importMapBaseURL, `
+ const t = async_test("${specifier}: dynamic import (from ${type})");
+ const handlers = getHandlers(t, "${specifier}", "${expected}");
+ const script = document.createElement("script");
+ script.setAttribute("type", "${type}");
+ script.innerText =
+ "import(\\"${specifier}\\")" +
+ ".then(handlers[Handler.DynamicImportResolve], " +
+ "handlers[Handler.DynamicImportReject]);";
+ script.addEventListener("error",
+ t.unreached_func("top-level inline script shouldn't error"));
+ document.body.appendChild(script);
+ `);
+}
+
+function testInIframeInjectBase(importMapString, importMapBaseURL, testScript) {
+ const iframe = document.createElement('iframe');
+ document.body.appendChild(iframe);
+ let content = `
+ <script src="/resources/testharness.js"></script>
+ <script src="/import-maps/resources/test-helper.js"></script>
+ <script src="/import-maps/resources/inject-base.js?pipe=sub&baseurl=${importMapBaseURL}"></script>
+ <script type="importmap">
+ ${importMapString}
+ </script>
+ <body>
+ <script>
+ setup({ allow_uncaught_exception: true });
+ ${testScript}
+ </sc` + `ript>
+ `;
+ iframe.contentDocument.write(content);
+ iframe.contentDocument.close();
+ return fetch_tests_from_window(iframe.contentWindow);
+}
+
+function testStaticImportInjectBase(importMapString, importMapBaseURL, specifier, expected) {
+ return testInIframeInjectBase(importMapString, importMapBaseURL, `
+ const t = async_test("${specifier}: static import with inject <base>");
+ const handlers = getHandlers(t, "${specifier}", "${expected}");
+ const script = document.createElement("script");
+ script.setAttribute("type", "module");
+ script.setAttribute("src",
+ "/import-maps/static-import.py?url=" +
+ encodeURIComponent("${specifier}"));
+ script.addEventListener("load", handlers[Handler.ScriptLoadEvent]);
+ script.addEventListener("error", handlers[Handler.ScriptErrorEvent]);
+ window.addEventListener("error", handlers[Handler.WindowErrorEvent]);
+ document.body.appendChild(script);
+ `);
+}
+
+function testDynamicImportInjectBase(importMapString, importMapBaseURL, specifier, expected, type) {
+ return testInIframeInjectBase(importMapString, importMapBaseURL, `
+ const t = async_test("${specifier}: dynamic import (from ${type}) with inject <base>");
+ const handlers = getHandlers(t, "${specifier}", "${expected}");
+ const script = document.createElement("script");
+ script.setAttribute("type", "${type}");
+ script.innerText =
+ "import(\\"${specifier}\\")" +
+ ".then(handlers[Handler.DynamicImportResolve], " +
+ "handlers[Handler.DynamicImportReject]);";
+ script.addEventListener("error",
+ t.unreached_func("top-level inline script shouldn't error"));
+ document.body.appendChild(script);
+ `);
+}
+
+function doTests(importMapString, importMapBaseURL, tests) {
+ promise_setup(function () {
+ return new Promise((resolve) => {
+ window.addEventListener("load", async () => {
+ for (const specifier in tests) {
+ // <script src> (module scripts)
+ await testScriptElement(importMapString, importMapBaseURL, specifier, tests[specifier][0], "module");
+
+ // <script src> (classic scripts)
+ await testScriptElement(importMapString, importMapBaseURL, specifier, tests[specifier][1], "text/javascript");
+
+ // static imports.
+ await testStaticImport(importMapString, importMapBaseURL, specifier, tests[specifier][2]);
+
+ // dynamic imports from a module script.
+ await testDynamicImport(importMapString, importMapBaseURL, specifier, tests[specifier][3], "module");
+
+ // dynamic imports from a classic script.
+ await testDynamicImport(importMapString, importMapBaseURL, specifier, tests[specifier][3], "text/javascript");
+ }
+ done();
+ resolve();
+ });
+ });
+ }, { explicit_done: true });
+}
diff --git a/testing/web-platform/tests/import-maps/script-supports-importmap.html b/testing/web-platform/tests/import-maps/script-supports-importmap.html
new file mode 100644
index 0000000000..026c052f99
--- /dev/null
+++ b/testing/web-platform/tests/import-maps/script-supports-importmap.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html>
+<title>HTMLScriptElement.supports importmap</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+test(function() {
+ assert_true(HTMLScriptElement.supports('importmap'));
+}, 'HTMLScriptElement.supports returns true for \'importmap\'');
+
+test(function() {
+ assert_false(HTMLScriptElement.supports(' importmap'));
+ assert_false(HTMLScriptElement.supports('importmap '));
+ assert_false(HTMLScriptElement.supports('Importmap'));
+ assert_false(HTMLScriptElement.supports('ImportMap'));
+ assert_false(HTMLScriptElement.supports('importMap'));
+ assert_false(HTMLScriptElement.supports('import-map'));
+ assert_false(HTMLScriptElement.supports('importmaps'));
+ assert_false(HTMLScriptElement.supports('import-maps'));
+}, 'HTMLScriptElement.supports returns false for unsupported types');
+
+</script>
diff --git a/testing/web-platform/tests/import-maps/static-import.py b/testing/web-platform/tests/import-maps/static-import.py
new file mode 100644
index 0000000000..fcf823c113
--- /dev/null
+++ b/testing/web-platform/tests/import-maps/static-import.py
@@ -0,0 +1,11 @@
+# This file needs to be a sibling of the test files (and not under resources/)
+# so that base URL resolution is the same between those test files and <script>s
+# pointing to this file.
+
+from wptserve.utils import isomorphic_decode
+
+def main(request, response):
+ return (
+ ((b'Content-Type', b'text/javascript'),),
+ u'import "{}";\n'.format(isomorphic_decode(request.GET.first(b'url')))
+ )