Commit ea19d09e authored by Hiroshige Hayashizaki's avatar Hiroshige Hayashizaki Committed by Commit Bot

[WPT] Migrate most of import-maps resolution tests out of blink internals

This CL migrates most of import maps resolution tests by
observing the resolution results by intercepting
module script requests by a service worker.

This CL also introduces `useInternalMethods` flag to clarify
the tests still requiring internal methods.

Bug: 1026809
Change-Id: I16c2a87bb67b530dc97b1631f6968b2d3bafdac6
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2457526
Commit-Queue: Hiroshige Hayashizaki <hiroshige@chromium.org>
Reviewed-by: default avatarDomenic Denicola <domenic@chromium.org>
Cr-Commit-Position: refs/heads/master@{#818366}
parent 4c5e65ea
......@@ -2753,8 +2753,9 @@ crbug.com/1050754 external/wpt/idle-detection/interceptor.https.html [ Failure ]
crbug.com/1050754 external/wpt/import-maps/acquire-import-maps-flag/dynamic-import/success.tentative.html [ Timeout ]
crbug.com/1050754 external/wpt/import-maps/acquire-import-maps-flag/script-tag/success.tentative.html [ Timeout ]
crbug.com/1050754 external/wpt/import-maps/acquire-import-maps-flag/worker-request/success.tentative.html [ Timeout ]
crbug.com/1050754 external/wpt/import-maps/common/parsing.tentative.html [ Failure ]
crbug.com/1050754 external/wpt/import-maps/common/resolving.tentative.html [ Failure ]
crbug.com/1050754 external/wpt/import-maps/common/parsing.tentative.https.html [ Failure ]
crbug.com/1050754 external/wpt/import-maps/common/resolving.tentative.https.html [ Failure ]
crbug.com/1050754 external/wpt/import-maps/common/resolving-internal.tentative.https.html [ Failure ]
crbug.com/1050754 external/wpt/import-maps/core/bare.sub.tentative.html [ Failure ]
crbug.com/1050754 external/wpt/import-maps/core/data.sub.tentative.html [ Failure ]
crbug.com/1050754 external/wpt/import-maps/core/http.sub.tentative.html [ Failure ]
......
......@@ -2657,8 +2657,9 @@ crbug.com/1050754 external/wpt/idle-detection/interceptor.https.html [ Failure ]
crbug.com/1050754 external/wpt/import-maps/acquire-import-maps-flag/dynamic-import/success.tentative.html [ Timeout ]
crbug.com/1050754 external/wpt/import-maps/acquire-import-maps-flag/script-tag/success.tentative.html [ Timeout ]
crbug.com/1050754 external/wpt/import-maps/acquire-import-maps-flag/worker-request/success.tentative.html [ Timeout ]
crbug.com/1050754 external/wpt/import-maps/common/parsing.tentative.html [ Failure ]
crbug.com/1050754 external/wpt/import-maps/common/resolving.tentative.html [ Failure ]
crbug.com/1050754 external/wpt/import-maps/common/parsing.tentative.https.html [ Failure ]
crbug.com/1050754 external/wpt/import-maps/common/resolving.tentative.https.html [ Failure ]
crbug.com/1050754 external/wpt/import-maps/common/resolving-internal.tentative.https.html [ Failure ]
crbug.com/1050754 external/wpt/import-maps/core/bare.sub.tentative.html [ Failure ]
crbug.com/1050754 external/wpt/import-maps/core/data.sub.tentative.html [ Failure ]
crbug.com/1050754 external/wpt/import-maps/core/http.sub.tentative.html [ Failure ]
......
......@@ -2870,8 +2870,9 @@ crbug.com/1050754 external/wpt/idle-detection/interceptor.https.html [ Failure ]
crbug.com/1050754 external/wpt/import-maps/acquire-import-maps-flag/dynamic-import/success.tentative.html [ Timeout ]
crbug.com/1050754 external/wpt/import-maps/acquire-import-maps-flag/script-tag/success.tentative.html [ Timeout ]
crbug.com/1050754 external/wpt/import-maps/acquire-import-maps-flag/worker-request/success.tentative.html [ Timeout ]
crbug.com/1050754 external/wpt/import-maps/common/parsing.tentative.html [ Failure ]
crbug.com/1050754 external/wpt/import-maps/common/resolving.tentative.html [ Failure ]
crbug.com/1050754 external/wpt/import-maps/common/parsing.tentative.https.html [ Failure ]
crbug.com/1050754 external/wpt/import-maps/common/resolving.tentative.https.html [ Failure ]
crbug.com/1050754 external/wpt/import-maps/common/resolving-internal.tentative.https.html [ Failure ]
crbug.com/1050754 external/wpt/import-maps/core/bare.sub.tentative.html [ Failure ]
crbug.com/1050754 external/wpt/import-maps/core/data.sub.tentative.html [ Failure ]
crbug.com/1050754 external/wpt/import-maps/core/http.sub.tentative.html [ Failure ]
......
let serveImporterScript = false;
self.addEventListener('message', event => {
serveImporterScript = true;
event.source.postMessage('Done');
});
self.addEventListener('fetch', event => {
if (event.request.url.indexOf('common-test-helper-iframe.js') >= 0) {
return;
}
if (serveImporterScript) {
serveImporterScript = false;
event.respondWith(
new Response(
'window.importHelper = (specifier) => import(specifier);',
{headers: {'Content-Type': 'text/javascript'}}
));
} else {
event.respondWith(
new Response(
'export const response = ' +
JSON.stringify({url: event.request.url}) + ';',
{headers: {'Access-Control-Allow-Origin': '*',
'Content-Type': 'text/javascript'}}
));
}
});
......@@ -2,9 +2,16 @@
<meta name="timeout" content="long">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
<script>
// All parsing tests requires Chromium's internal methods.
globalThis.useInternalMethods = true;
</script>
<body>
<script type="module">
import { runTestsFromJSON } from "./resources/common-test-helper.js";
import { runTestsFromJSON, setupGlobalCleanup } from "./resources/common-test-helper.js";
const promises = [];
for (const json of [
'resources/parsing-addresses-absolute.json',
......@@ -19,8 +26,13 @@ for (const json of [
'resources/parsing-specifier-keys.json',
'resources/parsing-trailing-slashes.json',
]) {
promise_test(() =>
runTestsFromJSON(json),
promise_test(() => {
const promise = runTestsFromJSON(json);
promises.push(promise);
return promise;
},
"Test helper: fetching and sanity checking test JSON: " + json);
}
Promise.all(promises).then(setupGlobalCleanup);
</script>
<!DOCTYPE html>
<meta name="timeout" content="long">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
<script>
// This test file is for resolution tests that require Chromium's internal
// methods.
// For tests that don't use Chromium's internal methods, see
// resolving.tentative.https.html.
globalThis.useInternalMethods = true;
</script>
<body>
<script type="module">
import { runTestsFromJSON, setupGlobalCleanup } from "./resources/common-test-helper.js";
const promises = [];
for (const json of [
'resources/empty-import-map-internal.json',
]) {
promise_test(() => {
const promise = runTestsFromJSON(json);
promises.push(promise);
return promise;
},
"Test helper: fetching and sanity checking test JSON: " + json);
}
Promise.all(promises).then(setupGlobalCleanup);
</script>
......@@ -2,9 +2,12 @@
<meta name="timeout" content="long">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
<body>
<script type="module">
import { runTestsFromJSON } from "./resources/common-test-helper.js";
import { runTestsFromJSON, setupGlobalCleanup } from "./resources/common-test-helper.js";
const promises = [];
for (const json of [
'resources/scopes.json',
......@@ -17,8 +20,13 @@ for (const json of [
'resources/overlapping-entries.json',
'resources/resolving-null.json',
]) {
promise_test(() =>
runTestsFromJSON(json),
promise_test(() => {
const promise = runTestsFromJSON(json);
promises.push(promise);
return promise;
},
"Test helper: fetching and sanity checking test JSON: " + json);
}
Promise.all(promises).then(setupGlobalCleanup);
</script>
// Handle errors around fetching, parsing and registering import maps.
const 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 === 'prepareResolve') {
// To get the result of #resolve-a-module-specifier given a script
// (with base URL = |baseURL|) and |specifier|, the service worker
// first serves an importer script with response URL = |baseURL|:
// window.importHelper = (specifier) => import(specifier);
// This is to use |baseURL| as the referringScript's base URL.
// Step 1. Signal the service worker to serve
// the importer script for the next fetch request.
parent.worker.postMessage('serveImporterScript');
} else if (event.data.action === 'resolve') {
if (event.data.expectedURL === null ||
new URL(event.data.expectedURL).protocol === 'https:') {
// Testing without internal methods:
// If the resolution is expected to fail (null case here),
// we can test the failure just by catching the exception.
// If the expected URL is HTTPS, we can test the result by
// intercepting requests by service workers.
// Step 3. Evaluate the importer script as a classic script,
// in order to prevent |baseURL| from being mapped by import maps.
const script = document.createElement('script');
script.onload = () => {
// Step 4. Trigger dynamic import from |baseURL|.
importHelper(event.data.specifier)
.then(module => {
// Step 5. Service worker responds with a JSON containing
// the request URL for the dynamic import
// (= the result of #resolve-a-module-specifier).
parent.postMessage({type: 'ResolutionSuccess',
result: module.response.url},
'*');
})
.catch(e => {
parent.postMessage(
{type: 'Failure', result: e.name, message: e.message},
'*');
});
};
script.src = event.data.baseURL;
document.body.appendChild(script);
} else {
// Testing with internal methods.
// For example, the resolution results are data: URLs.
if (!event.data.useInternalMethods) {
parent.postMessage(
{type: 'Failure',
result: 'Error',
message: 'internals.resolveModuleSpecifier is not available'},
'*');
return;
}
try {
const result = internals.resolveModuleSpecifier(
event.data.specifier,
event.data.baseURL,
document);
parent.postMessage(
{type: 'ResolutionSuccess', result: result}, '*');
} catch (e) {
parent.postMessage(
{type: 'Failure', result: e.name, message: e.message}, '*');
}
}
} else if (event.data.action === 'getParsedImportMap') {
if (!event.data.useInternalMethods) {
parent.postMessage(
{type: 'Failure',
result: 'Error',
message: 'internals.getParsedImportMap is not available'},
'*');
}
try {
parent.postMessage({
type: 'GetParsedImportMapSuccess',
result: internals.getParsedImportMap(document)}, '*');
} catch (e) {
parent.postMessage(
{type: 'Failure', result: e.name, message: e.message}, '*');
}
} else {
parent.postMessage({
type: 'Failure',
result: 'Error',
message: 'Invalid Action: ' + event.data.action}, '*');
}
});
setup({allow_uncaught_exception : true});
// Set window.useInternalMethods = true when needed && available.
let registration;
const scope = './scope/';
// Global setup: this must be the first promise_test.
promise_test(async (t) => {
const script = 'common-test-service-worker.js';
registration =
await service_worker_unregister_and_register(t, script, scope);
window.worker = registration.installing;
await wait_for_state(t, window.worker, 'activated');
}, 'global setup');
export function setupGlobalCleanup() {
// Global cleanup: the final promise_test.
promise_test(() => {
return registration.unregister();
}, 'global cleanup');
}
// Creates a new Document (via <iframe>) and add an inline import map.
function parse(importMap, importMapBaseURL) {
return new Promise(resolve => {
......@@ -15,79 +37,51 @@ function parse(importMap, importMapBaseURL) {
{once: true});
const testHTML = `
<script>
// Handle errors around fetching, parsing and registering import maps.
let registrationResult;
const onScriptError = event => {
registrationResult = {type: 'FetchError', error: event.error};
return false;
};
const windowErrorHandler = event => {
registrationResult = {type: 'ParseError', error: event.error};
return false;
};
window.addEventListener('error', windowErrorHandler);
window.addEventListener('load', event => {
if (!registrationResult) {
registrationResult = {type: 'Success'};
}
window.removeEventListener('error', windowErrorHandler);
parent.postMessage(registrationResult, '*');
});
// Handle specifier resolution requests from the parent frame.
window.addEventListener('message', event => {
try {
if (event.data.action === 'resolve') {
// URL resolution is tested using Chromium's internals.
// TODO(hiroshige): Remove the Chromium-specific dependency.
const result = internals.resolveModuleSpecifier(
event.data.specifier,
event.data.baseURL,
document);
parent.postMessage({type: 'ResolutionSuccess', result: result}, '*');
} else if (event.data.action === 'getParsedImportMap') {
parent.postMessage({
type: 'GetParsedImportMapSuccess',
result: internals.getParsedImportMap(document)}, '*');
} else {
parent.postMessage({
type: 'Failure',
result: "Invalid Action: " + event.data.action}, '*');
}
} catch (e) {
// We post error names instead of error objects themselves and
// re-create error objects later, to avoid issues around serializing
// error objects which is a quite new feature.
parent.postMessage({type: 'Failure', result: e.name}, '*');
}
});
</script>
<body>
<script src="${location.origin}/import-maps/common/resources/common-test-helper-iframe.js"></script>
<script type="importmap" onerror="onScriptError(event)">
${importMapString}
</script>
<script type="module">
if (!window.registrationResult) {
window.registrationResult = {type: 'Success'};
}
window.removeEventListener('error', window.windowErrorHandler);
parent.postMessage(window.registrationResult, '*');
</script>
</body>
`;
if (new URL(importMapBaseURL).protocol === 'data:') {
if (!window.useInternalMethods) {
throw new Error(
'Import maps with base URL = data: URL requires internal methods');
}
iframe.src = 'data:text/html;base64,' + btoa(testHTML);
} else {
iframe.srcdoc = `<base href="${importMapBaseURL}">` + testHTML;
// Set the src to `scope` in order to make requests from `iframe`
// intercepted by the service worker.
iframe.src = scope;
iframe.onload = () => {
iframe.contentDocument.write(
`<base href="${importMapBaseURL}">` + testHTML);
iframe.contentDocument.close();
};
}
document.body.appendChild(iframe);
});
}
// Returns a promise that is resolved with the resulting URL.
function resolve(specifier, parsedImportMap, baseURL) {
// `expectedURL` is a string, or null if to be thrown.
function resolve(specifier, parsedImportMap, baseURL, expectedURL) {
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());
reject(new TypeError(event.data.message));
} else {
reject(new Error(event.data.result));
}
......@@ -97,8 +91,20 @@ function resolve(specifier, parsedImportMap, baseURL) {
},
{once: true});
parsedImportMap.contentWindow.postMessage(
{action: "resolve", specifier: specifier, baseURL: baseURL}, '*');
parsedImportMap.contentWindow.postMessage({action: 'prepareResolve'}, '*');
navigator.serviceWorker.addEventListener('message', event => {
// Step 2. After postMessage() at Step 1 is processed, the service worker
// sends back a message and the parent Window receives the message here
// and sends a 'resolve' message to the iframe.
parsedImportMap.contentWindow.postMessage(
{action: 'resolve',
specifier: specifier,
baseURL: baseURL,
expectedURL: expectedURL,
useInternalMethods: window.useInternalMethods},
'*');
}, {once: true});
});
}
......@@ -112,14 +118,15 @@ function getParsedImportMap(parsedImportMap) {
{once: true});
parsedImportMap.contentWindow.postMessage(
{action: "getParsedImportMap"}, '*');
{action: 'getParsedImportMap',
useInternalMethods: window.useInternalMethods}, '*');
});
}
function assert_no_extra_properties(object, expectedProperties, description) {
for (const actualProperty in object) {
assert_true(expectedProperties.indexOf(actualProperty) !== -1,
description + ": unexpected property " + actualProperty);
description + ': unexpected property ' + actualProperty);
}
}
......@@ -184,18 +191,18 @@ async function runTests(j) {
assert_own_property(j, 'baseURL');
assert_equals(
j.parsedImportMap.parseImportMapResult,
"Success",
"Import map registration should be successful for resolution tests");
'Success',
'Import map registration should be successful for resolution tests');
for (const specifier in j.expectedResults) {
const expected = j.expectedResults[specifier];
promise_test(async t => {
if (expected === null) {
return promise_rejects_js(t, TypeError,
resolve(specifier, j.parsedImportMap, j.baseURL));
resolve(specifier, j.parsedImportMap, j.baseURL, null));
} else {
// Should be resolved to `expected`.
const actual = await resolve(
specifier, j.parsedImportMap, j.baseURL);
specifier, j.parsedImportMap, j.baseURL, expected);
assert_equals(actual, expected);
}
},
......@@ -207,7 +214,7 @@ async function runTests(j) {
if (j.hasOwnProperty('expectedParsedImportMap')) {
promise_test(async t => {
if (j.expectedParsedImportMap === null) {
assert_equals(j.parsedImportMap.parseImportMapResult, "ParseError");
assert_equals(j.parsedImportMap.parseImportMapResult, 'ParseError');
} else {
const actualParsedImportMap =
await getParsedImportMap(j.parsedImportMap);
......
{
"importMap": {},
"importMapBaseURL": "https://example.com/app/index.html",
"baseURL": "https://example.com/js/app.mjs",
"tests": {
"non-HTTPS fetch scheme absolute URLs": {
"expectedResults": {
"about:fetch-scheme": "about:fetch-scheme"
}
},
"non-fetch scheme absolute URLs": {
"expectedResults": {
"mailto:non-fetch-scheme": "mailto:non-fetch-scheme",
"import:non-fetch-scheme": "import:non-fetch-scheme",
"javascript:non-fetch-scheme": "javascript:non-fetch-scheme",
"wss:non-fetch-scheme": "wss://non-fetch-scheme/"
}
}
}
}
......@@ -18,23 +18,14 @@
"/../foo/../bar": "https://example.com/bar"
}
},
"fetch scheme absolute URLs": {
"HTTPS scheme absolute URLs": {
"expectedResults": {
"about:fetch-scheme": "about:fetch-scheme",
"https://fetch-scheme.net": "https://fetch-scheme.net/",
"https:fetch-scheme.org": "https://fetch-scheme.org/",
"https://fetch%2Dscheme.com/": "https://fetch-scheme.com/",
"https://///fetch-scheme.com///": "https://fetch-scheme.com///"
}
},
"non-fetch scheme absolute URLs": {
"expectedResults": {
"mailto:non-fetch-scheme": "mailto:non-fetch-scheme",
"import:non-fetch-scheme": "import:non-fetch-scheme",
"javascript:non-fetch-scheme": "javascript:non-fetch-scheme",
"wss:non-fetch-scheme": "wss://non-fetch-scheme/"
}
},
"valid relative URLs that are invalid as specifiers should fail": {
"expectedResults": {
"invalid-specifier": null,
......
......@@ -690,7 +690,7 @@ MISSING-LINK: css/geometry/*.worker.js
MISSING-LINK: css/filter-effects/*.any.js
# Tests that use WebKit/Blink testing APIs
LAYOUTTESTS APIS: import-maps/common/resources/common-test-helper.js
LAYOUTTESTS APIS: import-maps/common/resources/common-test-helper-iframe.js
LAYOUTTESTS APIS: resources/chromium/enable-hyperlink-auditing.js
LAYOUTTESTS APIS: resources/chromium/generic_sensor_mocks.js
LAYOUTTESTS APIS: resources/chromium/webxr-test.js
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment