feat(Response): add Response.fromCache / Response.fromServiceWorker (#1971)

This patch:
- introduces `test/assets/cached` folder and teaches server to cache
  all the assets from the folder
- introduces `test/assets/serviceworkers` folder that stores all the
  service workers and makes them register with unique URL prefix
- introduces `Response.fromCache()` and `Response.fromServiceWorker()`
  methods

Fixes #1551.
This commit is contained in:
Andrey Lushnikov 2018-02-05 17:59:07 -05:00 committed by GitHub
parent 660b65780f
commit ecc3adc279
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 140 additions and 15 deletions

View File

@ -200,6 +200,8 @@
* [request.url()](#requesturl) * [request.url()](#requesturl)
- [class: Response](#class-response) - [class: Response](#class-response)
* [response.buffer()](#responsebuffer) * [response.buffer()](#responsebuffer)
* [response.fromCache()](#responsefromcache)
* [response.fromServiceWorker()](#responsefromserviceworker)
* [response.headers()](#responseheaders) * [response.headers()](#responseheaders)
* [response.json()](#responsejson) * [response.json()](#responsejson)
* [response.ok()](#responseok) * [response.ok()](#responseok)
@ -2319,6 +2321,16 @@ page.on('request', request => {
#### response.buffer() #### response.buffer()
- returns: <Promise<[Buffer]>> Promise which resolves to a buffer with response body. - returns: <Promise<[Buffer]>> Promise which resolves to a buffer with response body.
#### response.fromCache()
- returns: <[boolean]>
True if the response was served from either the browser's disk cache or memory cache.
#### response.fromServiceWorker()
- returns: <[boolean]>
True if the response was served by a service worker.
#### response.headers() #### response.headers()
- returns: <[Object]> An object with HTTP headers associated with the response. All header names are lower-case. - returns: <[Object]> An object with HTTP headers associated with the response. All header names are lower-case.

View File

@ -48,6 +48,7 @@ class NetworkManager extends EventEmitter {
this._client.on('Network.requestWillBeSent', this._onRequestWillBeSent.bind(this)); this._client.on('Network.requestWillBeSent', this._onRequestWillBeSent.bind(this));
this._client.on('Network.requestIntercepted', this._onRequestIntercepted.bind(this)); this._client.on('Network.requestIntercepted', this._onRequestIntercepted.bind(this));
this._client.on('Network.requestServedFromCache', this._onRequestServedFromCache.bind(this));
this._client.on('Network.responseReceived', this._onResponseReceived.bind(this)); this._client.on('Network.responseReceived', this._onResponseReceived.bind(this));
this._client.on('Network.loadingFinished', this._onLoadingFinished.bind(this)); this._client.on('Network.loadingFinished', this._onLoadingFinished.bind(this));
this._client.on('Network.loadingFailed', this._onLoadingFailed.bind(this)); this._client.on('Network.loadingFailed', this._onLoadingFailed.bind(this));
@ -152,7 +153,7 @@ class NetworkManager extends EventEmitter {
if (event.redirectUrl) { if (event.redirectUrl) {
const request = this._interceptionIdToRequest.get(event.interceptionId); const request = this._interceptionIdToRequest.get(event.interceptionId);
if (request) { if (request) {
this._handleRequestRedirect(request, event.responseStatusCode, event.responseHeaders); this._handleRequestRedirect(request, event.responseStatusCode, event.responseHeaders, false /* fromDiskCache */, false /* fromServiceWorker */);
this._handleRequestStart(request._requestId, event.interceptionId, event.redirectUrl, event.resourceType, event.request, event.frameId); this._handleRequestStart(request._requestId, event.interceptionId, event.redirectUrl, event.resourceType, event.request, event.frameId);
} }
return; return;
@ -168,13 +169,24 @@ class NetworkManager extends EventEmitter {
} }
} }
/**
* @param {!Object} event
*/
_onRequestServedFromCache(event) {
const request = this._requestIdToRequest.get(event.requestId);
if (request)
request._fromMemoryCache = true;
}
/** /**
* @param {!Request} request * @param {!Request} request
* @param {number} redirectStatus * @param {number} redirectStatus
* @param {!Object} redirectHeaders * @param {!Object} redirectHeaders
* @param {boolean} fromDiskCache
* @param {boolean} fromServiceWorker
*/ */
_handleRequestRedirect(request, redirectStatus, redirectHeaders) { _handleRequestRedirect(request, redirectStatus, redirectHeaders, fromDiskCache, fromServiceWorker) {
const response = new Response(this._client, request, redirectStatus, redirectHeaders); const response = new Response(this._client, request, redirectStatus, redirectHeaders, fromDiskCache, fromServiceWorker);
request._response = response; request._response = response;
this._requestIdToRequest.delete(request._requestId); this._requestIdToRequest.delete(request._requestId);
this._interceptionIdToRequest.delete(request._interceptionId); this._interceptionIdToRequest.delete(request._interceptionId);
@ -227,7 +239,7 @@ class NetworkManager extends EventEmitter {
const request = this._requestIdToRequest.get(event.requestId); const request = this._requestIdToRequest.get(event.requestId);
// If we connect late to the target, we could have missed the requestWillBeSent event. // If we connect late to the target, we could have missed the requestWillBeSent event.
if (request) if (request)
this._handleRequestRedirect(request, event.redirectResponse.status, event.redirectResponse.headers); this._handleRequestRedirect(request, event.redirectResponse.status, event.redirectResponse.headers, event.redirectResponse.fromDiskCache, event.redirectResponse.fromServiceWorker);
} }
this._handleRequestStart(event.requestId, null, event.request.url, event.type, event.request, event.frameId); this._handleRequestStart(event.requestId, null, event.request.url, event.type, event.request, event.frameId);
} }
@ -240,7 +252,8 @@ class NetworkManager extends EventEmitter {
// FileUpload sends a response without a matching request. // FileUpload sends a response without a matching request.
if (!request) if (!request)
return; return;
const response = new Response(this._client, request, event.response.status, event.response.headers); const response = new Response(this._client, request, event.response.status, event.response.headers,
event.response.fromDiskCache, event.response.fromServiceWorker);
request._response = response; request._response = response;
this.emit(NetworkManager.Events.Response, response); this.emit(NetworkManager.Events.Response, response);
} }
@ -310,6 +323,8 @@ class Request {
this._frame = frame; this._frame = frame;
for (const key of Object.keys(payload.headers)) for (const key of Object.keys(payload.headers))
this._headers[key.toLowerCase()] = payload.headers[key]; this._headers[key.toLowerCase()] = payload.headers[key];
this._fromMemoryCache = false;
} }
/** /**
@ -483,14 +498,18 @@ class Response {
* @param {!Request} request * @param {!Request} request
* @param {number} status * @param {number} status
* @param {!Object} headers * @param {!Object} headers
* @param {boolean} fromDiskCache
* @param {boolean} fromServiceWorker
*/ */
constructor(client, request, status, headers) { constructor(client, request, status, headers, fromDiskCache, fromServiceWorker) {
this._client = client; this._client = client;
this._request = request; this._request = request;
this._contentPromise = null; this._contentPromise = null;
this._status = status; this._status = status;
this._url = request.url(); this._url = request.url();
this._fromDiskCache = fromDiskCache;
this._fromServiceWorker = fromServiceWorker;
this._headers = {}; this._headers = {};
for (const key of Object.keys(headers)) for (const key of Object.keys(headers))
this._headers[key.toLowerCase()] = headers[key]; this._headers[key.toLowerCase()] = headers[key];
@ -561,6 +580,20 @@ class Response {
request() { request() {
return this._request; return this._request;
} }
/**
* @return {boolean}
*/
fromCache() {
return this._fromDiskCache || this._request._fromMemoryCache;
}
/**
* @return {boolean}
*/
fromServiceWorker() {
return this._fromServiceWorker;
}
} }
helper.tracePublicAPI(Response); helper.tracePublicAPI(Response);

View File

@ -0,0 +1,3 @@
body {
background-color: pink;
}

View File

@ -0,0 +1,2 @@
<link rel='stylesheet' href='./one-style.css'>
<div>hello, world!</div>

View File

@ -0,0 +1,3 @@
<script>
window.registrationPromise = navigator.serviceWorker.register('sw.js');
</script>

View File

@ -0,0 +1,3 @@
body {
background-color: pink;
}

View File

@ -0,0 +1,4 @@
<link rel="stylesheet" href="./style.css">
<script>
navigator.serviceWorker.register('sw.js');
</script>

View File

@ -0,0 +1,3 @@
self.addEventListener('fetch', event => {
event.respondWith(fetch(event.request));
});

View File

@ -68,6 +68,9 @@ class SimpleServer {
this._server.listen(port); this._server.listen(port);
this._dirPath = dirPath; this._dirPath = dirPath;
this._startTime = new Date();
this._cachedPathPrefix = null;
/** @type {!Set<!net.Socket>} */ /** @type {!Set<!net.Socket>} */
this._sockets = new Set(); this._sockets = new Set();
@ -90,6 +93,13 @@ class SimpleServer {
socket.once('close', () => this._sockets.delete(socket)); socket.once('close', () => this._sockets.delete(socket));
} }
/**
* @param {string} pathPrefix
*/
enableHTTPCache(pathPrefix) {
this._cachedPathPrefix = pathPrefix;
}
/** /**
* @param {string} path * @param {string} path
* @param {string} username * @param {string} username
@ -189,15 +199,27 @@ class SimpleServer {
let pathName = url.parse(request.url).path; let pathName = url.parse(request.url).path;
if (pathName === '/') if (pathName === '/')
pathName = '/index.html'; pathName = '/index.html';
pathName = path.join(this._dirPath, pathName.substring(1)); const filePath = path.join(this._dirPath, pathName.substring(1));
fs.readFile(pathName, function(err, data) { if (this._cachedPathPrefix !== null && filePath.startsWith(this._cachedPathPrefix)) {
if (err) { if (request.headers['if-modified-since']) {
response.statusCode = 404; response.statusCode = 304; // not modified
response.end(`File not found: ${pathName}`); response.end();
return; return;
} }
response.setHeader('Content-Type', mime.lookup(pathName)); response.setHeader('Cache-Control', 'public, max-age=31536000');
response.setHeader('Last-Modified', this._startTime.toString());
} else {
response.setHeader('Cache-Control', 'no-cache, no-store');
}
fs.readFile(filePath, function(err, data) {
if (err) {
response.statusCode = 404;
response.end(`File not found: ${filePath}`);
return;
}
response.setHeader('Content-Type', mime.lookup(filePath));
response.end(data); response.end(data);
}); });
} }

View File

@ -19,10 +19,14 @@ const SimpleServer = require('./SimpleServer');
const port = 8907; const port = 8907;
const httpsPort = 8908; const httpsPort = 8908;
const assetsPath = path.join(__dirname, '..', 'assets'); const assetsPath = path.join(__dirname, '..', 'assets');
const cachedPath = path.join(__dirname, '..', 'assets', 'cached');
Promise.all([ Promise.all([
SimpleServer.create(assetsPath, port), SimpleServer.create(assetsPath, port),
SimpleServer.createHTTPS(assetsPath, httpsPort) SimpleServer.createHTTPS(assetsPath, httpsPort)
]).then(([server, httpsServer]) => { ]).then(([server, httpsServer]) => {
server.enableHTTPCache(cachedPath);
httpsServer.enableHTTPCache(cachedPath);
console.log(`HTTP: server is running on http://localhost:${port}`); console.log(`HTTP: server is running on http://localhost:${port}`);
console.log(`HTTPS: server is running on https://localhost:${httpsPort}`); console.log(`HTTPS: server is running on https://localhost:${httpsPort}`);
}); });

View File

@ -80,14 +80,18 @@ if (fs.existsSync(OUTPUT_DIR))
beforeAll(async state => { beforeAll(async state => {
const assetsPath = path.join(__dirname, 'assets'); const assetsPath = path.join(__dirname, 'assets');
const cachedPath = path.join(__dirname, 'assets', 'cached');
const port = 8907 + state.parallelIndex * 2; const port = 8907 + state.parallelIndex * 2;
state.server = await SimpleServer.create(assetsPath, port); state.server = await SimpleServer.create(assetsPath, port);
state.server.enableHTTPCache(cachedPath);
state.server.PREFIX = `http://localhost:${port}`; state.server.PREFIX = `http://localhost:${port}`;
state.server.CROSS_PROCESS_PREFIX = `http://127.0.0.1:${port}`; state.server.CROSS_PROCESS_PREFIX = `http://127.0.0.1:${port}`;
state.server.EMPTY_PAGE = `http://localhost:${port}/empty.html`; state.server.EMPTY_PAGE = `http://localhost:${port}/empty.html`;
const httpsPort = port + 1; const httpsPort = port + 1;
state.httpsServer = await SimpleServer.createHTTPS(assetsPath, httpsPort); state.httpsServer = await SimpleServer.createHTTPS(assetsPath, httpsPort);
state.httpsServer.enableHTTPCache(cachedPath);
state.httpsServer.PREFIX = `https://localhost:${httpsPort}`; state.httpsServer.PREFIX = `https://localhost:${httpsPort}`;
state.httpsServer.CROSS_PROCESS_PREFIX = `https://127.0.0.1:${httpsPort}`; state.httpsServer.CROSS_PROCESS_PREFIX = `https://127.0.0.1:${httpsPort}`;
state.httpsServer.EMPTY_PAGE = `https://localhost:${httpsPort}/empty.html`; state.httpsServer.EMPTY_PAGE = `https://localhost:${httpsPort}/empty.html`;
@ -2663,8 +2667,40 @@ describe('Page', function() {
expect(responses[0].url()).toBe(server.EMPTY_PAGE); expect(responses[0].url()).toBe(server.EMPTY_PAGE);
expect(responses[0].status()).toBe(200); expect(responses[0].status()).toBe(200);
expect(responses[0].ok()).toBe(true); expect(responses[0].ok()).toBe(true);
expect(responses[0].fromCache()).toBe(false);
expect(responses[0].fromServiceWorker()).toBe(false);
expect(responses[0].request()).toBeTruthy(); expect(responses[0].request()).toBeTruthy();
}); });
it('Response.fromCache()', async({page, server}) => {
const responses = new Map();
page.on('response', r => responses.set(r.url().split('/').pop(), r));
// Load and re-load to make sure it's cached.
await page.goto(server.PREFIX + '/cached/one-style.html');
await page.reload();
expect(responses.size).toBe(2);
expect(responses.get('one-style.html').status()).toBe(304);
expect(responses.get('one-style.html').fromCache()).toBe(false);
expect(responses.get('one-style.css').status()).toBe(200);
expect(responses.get('one-style.css').fromCache()).toBe(true);
});
it('Response.fromServiceWorker', async({page, server}) => {
const responses = new Map();
page.on('response', r => responses.set(r.url().split('/').pop(), r));
// Load and re-load to make sure serviceworker is installed and running.
await page.goto(server.PREFIX + '/serviceworkers/fetch/sw.html', {waitUntil: 'networkidle2'});
await page.reload();
expect(responses.size).toBe(2);
expect(responses.get('sw.html').status()).toBe(200);
expect(responses.get('sw.html').fromServiceWorker()).toBe(true);
expect(responses.get('style.css').status()).toBe(200);
expect(responses.get('style.css').fromServiceWorker()).toBe(true);
});
it('Page.Events.Response should provide body', async({page, server}) => { it('Page.Events.Response should provide body', async({page, server}) => {
let response = null; let response = null;
page.on('response', r => response = r); page.on('response', r => response = r);
@ -3527,13 +3563,13 @@ describe('Page', function() {
it('should report when a service worker is created and destroyed', async({page, server, browser}) => { it('should report when a service worker is created and destroyed', async({page, server, browser}) => {
await page.goto(server.EMPTY_PAGE); await page.goto(server.EMPTY_PAGE);
const createdTarget = new Promise(fulfill => browser.once('targetcreated', target => fulfill(target))); const createdTarget = new Promise(fulfill => browser.once('targetcreated', target => fulfill(target)));
const registration = await page.evaluateHandle(() => navigator.serviceWorker.register('sw.js')); await page.goto(server.PREFIX + '/serviceworkers/empty/sw.html');
expect((await createdTarget).type()).toBe('service_worker'); expect((await createdTarget).type()).toBe('service_worker');
expect((await createdTarget).url()).toBe(server.PREFIX + '/sw.js'); expect((await createdTarget).url()).toBe(server.PREFIX + '/serviceworkers/empty/sw.js');
const destroyedTarget = new Promise(fulfill => browser.once('targetdestroyed', target => fulfill(target))); const destroyedTarget = new Promise(fulfill => browser.once('targetdestroyed', target => fulfill(target)));
await page.evaluate(registration => registration.unregister(), registration); await page.evaluate(() => window.registrationPromise.then(registration => registration.unregister()));
expect(await destroyedTarget).toBe(await createdTarget); expect(await destroyedTarget).toBe(await createdTarget);
}); });
it('should report when a target url changes', async({page, server, browser}) => { it('should report when a target url changes', async({page, server, browser}) => {