var express = require("@runkit/runkit/express-endpoint/1.0.0");
var app = express(exports);
const DELAY_TIME = 200;
const VARY_HEADER = 'Force-Reload';
app.set('etag', false);
app.set('x-powered-by', false);
// Serve the test endpoint (with delay to allow caching to be tested)
app.get('/test/:testid', (req, res) => {
res.set('Cache-Control', 'max-age=86400');
res.set('Vary', VARY_HEADER);
res.set('Content-type', 'text/html; charset=utf8');
setTimeout(() => {
res.end(Date.now() + ' ' + Math.random());
}, DELAY_TIME);
});
// Serve the Clear-Site-Data endpoint
app.get('/clear-site-data', (req, res) => {
res.set('Cache-Control', 'no-cache');
res.set('Clear-Site-Data', '"cache"');
res.end('OK');
});
// Serve the location.reload(true) clearer page
app.get('/reloader-frame', (req, res) => {
if (!req.query.url || req.query.url.match(/[^\w\/\.]/)) throw new Error ('Invalid param "url"');
res.set('Cache-Control', 'max-age=86400; immutable');
res.set('Content-type', 'text/html; charset=utf8');
res.end(`
<html>
<body>
<iframe id='frame' src='${req.query.url}'></iframe>
<script>
const ifr = document.getElementById('frame');
ifr.addEventListener('load', e => {
const content = ifr.contentWindow.document.body.innerHTML;
const saved = localStorage.getItem('cache-test-saved');
if (saved) {
localStorage.removeItem('cache-test-saved');
parent.postMessage(content, '*');
} else {
localStorage.setItem('cache-test-saved', content);
location.reload(true);
}
});
</script>
</body>
</html>
`);
});
// Serve the test page
app.get('/', (req, res) => {
res.set('Content-type', 'text/html; charset=utf-8');
res.set('Cache-control', 'no-cache');
res.end(`
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>Cache clearance tests</title>
<link rel='stylesheet' href='https://cdnjs.cloudflare.com/ajax/libs/mocha/4.0.1/mocha.css' />
<style>iframe { visibility: hidden; }</style>
<script src='https://cdnjs.cloudflare.com/ajax/libs/expect.js/0.2.0/expect.js'></script>
<script src='https://cdnjs.cloudflare.com/ajax/libs/mocha/4.0.1/mocha.js'></script>
</head>
<body>
<div id="mocha"></div>
<script>
const SERVER_DELAY = ${DELAY_TIME};
const SERVER_VARY = '${VARY_HEADER}';
const previousContent = {};
async function isLoadedFromCache(url, mechanism, config) {
let newcontent;
if (mechanism == 'fetch') {
config = config || {};
config.credentials = 'include';
newcontent = await fetch(url, config).then(resp => resp.text());
} else if (mechanism == 'nav') {
const ifr = document.createElement('iframe');
ifr.src = url;
document.body.appendChild(ifr);
await new Promise(resolve => ifr.addEventListener('load', resolve));
newcontent = ifr.contentWindow.document.body.innerHTML;
ifr.remove();
} else if (mechanism == 'reloader') {
const reloaderUrl = '/reloader-frame?url='+encodeURIComponent(url);
const ifr = document.createElement('iframe');
ifr.src = reloaderUrl;
document.body.appendChild(ifr);
newcontent = await new Promise(resolve => {
window.addEventListener('message', function handler(e) {
if (e.source === ifr.contentWindow) {
window.removeEventListener('message', handler);
resolve(e.data);
}
});
});
ifr.remove();
}
const result = previousContent[url] === newcontent;
previousContent[url] = newcontent;
return result;
}
mocha.setup({ timeout: 5000, ui: 'bdd' });
describe('Has a single working cache', function() {
const url1 = '/test/'+Math.floor(Math.random()*100000);
const url2 = '/test/'+Math.floor(Math.random()*100000);
it('should not be cached at the start of the run', async () => {
expect(await isLoadedFromCache(url1, 'fetch')).to.be(false);
expect(await isLoadedFromCache(url2, 'nav')).to.be(false);
});
it('should hit cache on second fetch', async () => {
expect(await isLoadedFromCache(url1, 'fetch')).to.be(true);
});
it('should hit cache on second navigation', async () => {
expect(await isLoadedFromCache(url2, 'nav')).to.be(true);
});
it('should hit cache on navigation if loaded by fetch', async () => {
expect(await isLoadedFromCache(url1, 'nav')).to.be(true);
});
it('should hit cache on fetch if loaded by navigation', async () => {
expect(await isLoadedFromCache(url2, 'fetch')).to.be(true);
});
});
describe('Supports clearing cache using Vary', function() {
const url = '/test/'+Math.floor(Math.random()*100000);
// Populates the cache
before(async function() {
await isLoadedFromCache(url, 'fetch');
});
it('starts out with cache populated', async () => {
expect(await isLoadedFromCache(url, 'fetch')).to.be(true);
});
it('should make a network request if varied header changes', async () => {
const options = { headers: {} };
options.headers[SERVER_VARY] = Math.random();
expect(await isLoadedFromCache(url, 'fetch', options)).to.be(false);
});
it('should hit the network again for the next fetch', async () => {
expect(await isLoadedFromCache(url, 'fetch')).to.be(false);
});
it('should hit cache for subsequent fetches', async () => {
expect(await isLoadedFromCache(url, 'fetch')).to.be(true);
});
it('should also use the new cache for subsequent navigations', async () => {
expect(await isLoadedFromCache(url, 'nav')).to.be(true);
});
});
describe('Supports clearing cache using FetchOptions.cache', function() {
const url = '/test/'+Math.floor(Math.random()*100000);
before(async function() {
await isLoadedFromCache(url, 'fetch');
});
it('starts out with cache populated', async () => {
expect(await isLoadedFromCache(url, 'fetch')).to.be(true);
});
it('should make a network request if FetchOptions.cache is set to "reload"', async () => {
const options = { cache: 'reload' };
expect(await isLoadedFromCache(url, 'fetch', options)).to.be(false);
});
it('should then hit cache on subsequent fetches', async () => {
expect(await isLoadedFromCache(url, 'fetch')).to.be(true);
});
it('should make a network request if FetchOptions.cache is set to "reload" (repeat)', async () => {
const options = { cache: 'reload' };
expect(await isLoadedFromCache(url, 'fetch', options)).to.be(false);
});
it('should also use the new cache for subsequent navigations', async () => {
expect(await isLoadedFromCache(url, 'nav')).to.be(true);
});
});
describe('Supports clearing cache using Clear-Site-Data', function() {
const url1 = '/test/'+Math.floor(Math.random()*100000);
const url2 = '/test/'+Math.floor(Math.random()*100000);
before(async function() {
await isLoadedFromCache(url1, 'fetch');
await isLoadedFromCache(url2, 'nav');
});
it('should hit network for a fetch if a Clear-Site-Data response is received first', async () => {
expect(await isLoadedFromCache(url1, 'fetch')).to.be(true);
await isLoadedFromCache('/clear-site-data', 'fetch', {"credentials": "same-origin"});
expect(await isLoadedFromCache(url1, 'fetch')).to.be(false);
});
it('should hit network for a navigation if a Clear-Site-Data response is received first', async () => {
expect(await isLoadedFromCache(url2, 'nav')).to.be(true);
await isLoadedFromCache('/clear-site-data', 'nav');
expect(await isLoadedFromCache(url2, 'nav')).to.be(false);
});
it('should clear nav cache even if CSD is received via fetch', async () => {
expect(await isLoadedFromCache(url2, 'nav')).to.be(true);
await isLoadedFromCache('/clear-site-data', 'fetch', {"credentials": "same-origin"});
expect(await isLoadedFromCache(url2, 'nav')).to.be(false);
});
it('should clear fetch cache even if CSD is received via nav', async () => {
expect(await isLoadedFromCache(url1, 'fetch')).to.be(true);
await isLoadedFromCache('/clear-site-data', 'nav');
expect(await isLoadedFromCache(url1, 'fetch')).to.be(false);
});
});
describe('Supports clearing cache using location.reload(true)', function() {
const url = '/test/'+Math.floor(Math.random()*100000);
before(async function() {
await isLoadedFromCache(url, 'nav');
});
it('starts out with cache populated', async () => {
expect(await isLoadedFromCache(url, 'nav')).to.be(true);
});
it('should hit network when doing location.reload(true)', async () => {
expect(await isLoadedFromCache(url, 'reloader')).to.be(false);
});
it('should hit cache subsequently', async () => {
expect(await isLoadedFromCache(url, 'nav')).to.be(true);
});
});
mocha.run();
</script>
</body>
</html>
`);
});
// Return a 404 if no routes match
app.use((req, res, next) => {
res.set('Cache-Control', 'max-age=0; private');
res.status(404).end('Not found');
});