diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 01:47:29 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 01:47:29 +0000 |
commit | 0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d (patch) | |
tree | a31f07c9bcca9d56ce61e9a1ffd30ef350d513aa /testing/web-platform/tests/import-maps/data-driven | |
parent | Initial commit. (diff) | |
download | firefox-esr-0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d.tar.xz firefox-esr-0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d.zip |
Adding upstream version 115.8.0esr.upstream/115.8.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/web-platform/tests/import-maps/data-driven')
26 files changed, 1889 insertions, 0 deletions
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() |