Skip to content

Commit

Permalink
Add tests to verify preload caching behavior
Browse files Browse the repository at this point in the history
Check that preloaded resources are reused/ignored based on the
parameters available to a preload link (href, crossorigin, as),
and do not rely on other parameters (e.g. whether a script is a module).

See whatwg/html#7260
and whatwg/fetch#1342
  • Loading branch information
noamr committed Dec 1, 2021
1 parent 029d8d6 commit c8a27d4
Show file tree
Hide file tree
Showing 3 changed files with 200 additions and 0 deletions.
34 changes: 34 additions & 0 deletions preload/preload-invalid-resources.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/common/utils.js"></script>
<script src="/preload/resources/preload_helper.js"></script>
<body>
<script>

const invalidImages = {
'invalid data': '/preload/resources/echo-with-cors.py?type=image/svg+xml&content=junk',
missing: '/nothing.png'
}

Object.entries(invalidImages).forEach(([name, url]) => {
promise_test(async t => {
const invalidImageURL = getAbsoluteURL(url)
const link = document.createElement('link');
link.rel = 'preload';
link.as = 'image';
link.href = url;
document.head.appendChild(link);
t.add_cleanup(() => link.remove());
await new Promise(resolve => {
const img = document.createElement('img');
img.src = url;
img.onerror = resolve;
document.body.appendChild(img);
t.add_cleanup(() => img.remove());
});
verifyNumberOfResourceTimingEntries(url, 1);
}, `Preloading an invalid image (${name}) should preload and not re-fetch`)
})

</script>
</body>
158 changes: 158 additions & 0 deletions preload/preload-resource-match.https.sub.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/common/utils.js"></script>
<script src="/preload/resources/preload_helper.js"></script>
<script>

const crossOriginHost = 'https://{{host}}:{{ports[https][0]}}'

function createEchoURL(text, type) {
return `/preload/resources/echo-with-cors.py?type=${
encodeURIComponent(type)}&content=${
encodeURIComponent(text)}`
}
const urls = {
image: createEchoURL('<svg xmlns="http://www.w3.org/2000/svg" width="2" height="2" />', 'image/svg+xml'),
font: '/preload/resources/Ahem.ttf?x',
text: createEchoURL('hello', 'text/plain'),
script: createEchoURL('function dummy() { }', 'application/javascript'),
style: createEchoURL('.cls { }', 'text/css'),
}

const resourceTypes = {
image: {url: urls.image, as: 'image'},
invalidImage: {url: urls.invalidImage, as: 'image'},
font: {url: urls.font, as: 'font', config: 'anonymous'},
backgroundImage: {url: urls.image, as: 'image', config: 'no-cors'},
fetch: {url: urls.text, as: 'fetch'},
script: {url: urls.script, as: 'script'},
module: {url: urls.script, as: 'script'},
// style: {url: urls.style, as: 'style'},
}

const configs = {
'same-origin': {crossOrigin: false, attributes: {}},
'no-cors': {crossOrigin: true, attributes: {}},
'anonymous': {crossOrigin: true, attributes: {crossOrigin: 'anonymous'}},
'use-credentials': {crossOrigin: true, attributes: {crossOrigin: 'use-credentials'}},
}

function preload(attributes, t) {
const link = document.createElement('link');
link.rel = "preload";
Object.entries(attributes).forEach(([key, value]) => {
if (value)
link[key] = value;
});

document.head.appendChild(link);
// t.add_cleanup(() => link.remove());
return new Promise(resolve => link.addEventListener('load', resolve));
}

const loaders = {
image: (href, attr, t) => {
const img = document.createElement('img');
Object.entries(attr).forEach(([key, value]) => {
img[key] = value;
});

img.src = href

document.body.appendChild(img);
t.add_cleanup(() => img.remove());
return new Promise(resolve => {
img.addEventListener('load', resolve)
img.addEventListener('error', resolve)
});
},
font: (href, attr, t) => {
const style = document.createElement('style');
style.innerHTML = `@font-face {
font-family: 'MyFont';
src: url('${href}');
}`;

document.head.appendChild(style);
t.add_cleanup(() => style.remove());
const p = document.createElement('p');
p.style.fontFamily = 'MyFont';
document.body.appendChild(p);
t.add_cleanup(() => p.remove());
},
shape: (href, attr, t) => {
const div = document.createElement('div');
div.style.shapeOutside = `url(${href})`;
document.body.appendChild(div);
t.add_cleanup(() => div.remove());
},
backgroundImage: (href, attr, t) => {
const div = document.createElement('div');
div.style.background = `url(${href})`;
document.body.appendChild(div);
t.add_cleanup(() => div.remove());
},
fetch: async (href, attr, t) => {
const options = {mode: attr.crossOrigin ? 'cors' : 'no-cors',
credentials: !attr.crossOrigin || attr.crossOrigin === 'anonymous' ? 'omit' : 'include'}

const response = await fetch(href, options)
await response.text();
},
script: async (href, attr, t) => {
const script = document.createElement('script');
t.add_cleanup(() => script.remove());
if (attr.crossOrigin)
script.setAttribute('crossorigin', attr.crossOrigin);
script.src = href;
document.body.appendChild(script);
await new Promise(resolve => { script.onload = resolve });
},
module: async (href, attr, t) => {
const script = document.createElement('script');
script.type = 'module';
t.add_cleanup(() => script.remove());
if (attr.crossOrigin)
script.setAttribute('crossorigin', attr.crossOrigin);
script.src = href;
document.body.appendChild(script);
await new Promise(resolve => { script.onload = resolve });
},
style: (href, attr, t) => {
const style = document.createElement('link');
style.rel = 'stylesheet';
t.add_cleanup(() => style.remove());
if (attr.crossOrigin)
style.setAttribute('crossorigin', attr.crossOrigin);
document.body.appendChild(style);
}

}

function preload_reuse_test(type, as, url, preloadConfig, resourceConfig) {
const expected = (preloadConfig === resourceConfig) ? "reuse" : "discard";
const key = token();
const href = getAbsoluteURL(`${
(configs[resourceConfig].crossOrigin ? crossOriginHost : '') + url
}&${token()}`)
promise_test(async t => {
await preload({href, as, ...configs[preloadConfig].attributes}, t);
await loaders[as](href, configs[resourceConfig].attributes, t)
verifyNumberOfResourceTimingEntries(href, expected === "reuse" ? 1 : 2)
}, `Loading ${type} (${resourceConfig}) with link (${preloadConfig}) should ${expected} the preloaded response`);
}

for (const resourceTypeName in resourceTypes) {
const resourceInfo = resourceTypes[resourceTypeName];
const configNames = resourceInfo.config ? [resourceInfo.config, 'same-origin'] : Object.keys(configs)
for (const resourceConfigName of configNames) {
for (const preloadConfigName of Object.keys(configs)) {
if (resourceConfigName === 'same-origin' && preloadConfigName !== 'same-origin')
continue;
if (resourceConfigName !== 'same-origin' && preloadConfigName === 'same-origin')
continue;
preload_reuse_test(resourceTypeName, resourceInfo.as, resourceInfo.url, preloadConfigName, resourceConfigName);
}
}
}
</script>
8 changes: 8 additions & 0 deletions preload/resources/echo-with-cors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
def main(request, response):
response.headers.set(b"Content-Type", request.GET.first(b"type"))
origin = request.headers.get('Origin')
if origin is not None:
response.headers.set(b"Access-Control-Allow-Origin", origin)
response.headers.set(b"Access-Control-Allow-Credentials", b"true")

return request.GET.first(b"content")

0 comments on commit c8a27d4

Please sign in to comment.