diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
commit | 43a97878ce14b72f0981164f87f2e35e14151312 (patch) | |
tree | 620249daf56c0258faa40cbdcf9cfba06de2a846 /testing/web-platform/tests/import-maps | |
parent | Initial commit. (diff) | |
download | firefox-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')
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'))) + ) |