diff --git a/lib/interceptor.js b/lib/interceptor.js index 44a44c97..928807d3 100644 --- a/lib/interceptor.js +++ b/lib/interceptor.js @@ -40,6 +40,12 @@ var interceptor = { window.sessionStorage.removeItem(NAMESPACE); } + if (typeof window.navigator.sendBeacon === 'function') { + replaceSendBeacon(); + } else { + console.error(PKG_PREFIX + 'sendBeacon API preconditions not met!'); + } + if (typeof window.fetch === 'function') { replaceFetch(); if ( @@ -54,6 +60,49 @@ var interceptor = { done(window[NAMESPACE]); + function replaceSendBeacon() { + var _sendBeacon = window.navigator.sendBeacon; + window.navigator.sendBeacon = function (target, data) { + var url = target; + + if (target instanceof URL) { + url = target.href; + } + + var request = { + method: 'POST', + requestHeaders: {}, + requestBody: data, + url: url + }; + + // While the SendBeacon API is synchronous--we do not _need_ to do the dance + // of registering a pending request and then immediately completing it--doing + // so allows us to preserve the method's synchronous API while still logging + // its possible payloads. + addPendingRequest(request); + + // TODO: we might need to clone the data in order to log it, for some kinds of input (e.g. buffers). + var queued = _sendBeacon.call(window.navigator, url, data); + handleDoneRequest(request, queued); + + // Forward the original response to the application on the current tick. + return queued; + }; + + function handleDoneRequest(rq, isQueued) { + var input = rq.requestBody; + if (input instanceof Blob) { + input.text().then(function (payload) { + rq.requestBody = payload; + completeSendBeaconRequest(rq, isQueued); + }); + } else { + completeSendBeaconRequest(rq, isQueued); + } + } + } + function replaceFetch() { var _fetch = window.fetch; window.fetch = function () { @@ -240,6 +289,12 @@ var interceptor = { pushToSessionStorage(startedRequest); } + function completeSendBeaconRequest(startedRequest, isQueued) { + startedRequest.statusCode = isQueued ? 200 : 500; + startedRequest.__fulfilled = Date.now(); + replaceInSessionStorage(startedRequest); + } + function completeFetchRequest(startedRequest, completedRequest) { // Merge the completed data with the started request. startedRequest.requestBody = completedRequest.requestBody; diff --git a/test/site/header-parsing.html b/test/site/header_parsing.html similarity index 100% rename from test/site/header-parsing.html rename to test/site/header_parsing.html diff --git a/test/site/send_beacon.html b/test/site/send_beacon.html new file mode 100644 index 00000000..00d0b316 --- /dev/null +++ b/test/site/send_beacon.html @@ -0,0 +1,44 @@ + + + + + + sendBeacon request + + +

This file makes various sendBeacon requests

+ + + +
+ + + diff --git a/test/spec/plugin_test.js b/test/spec/plugin_test.js index 388455a3..e3d21a4f 100644 --- a/test/spec/plugin_test.js +++ b/test/spec/plugin_test.js @@ -173,7 +173,7 @@ describe('webdriverajax', function testSuite() { it( 'can access response headers when response ' + config.when, async function () { - await browser.url('/header-parsing.html'); + await browser.url('/header_parsing.html'); await browser.setupInterceptor(); await completedRequest(config.buttonId); const request = await browser.getRequest(0); @@ -542,6 +542,54 @@ describe('webdriverajax', function testSuite() { }); }); + describe('sendBeacon API', async function () { + this.beforeEach(async function () { + await browser.url('/send_beacon.html'); + await browser.setupInterceptor(); + }); + + it('reports beacons as requests with POST method', async function () { + await completedRequest('#buttonstring'); + const requests = await browser.getRequests(); + assert.strictEqual(requests.length, 1, 'should capture single request'); + assert.strictEqual( + requests[0].method, + 'POST', + 'should capture POST request', + ); + }); + + it('can intercept when target is URL object', async function () { + await browser.expectRequest('POST', /\/telemetry\?data=querystring/, 200); + await completedRequest('#url'); + await browser.assertRequests(); + await browser.assertExpectedRequestsOnly(); + const action = await browser.$('#response').getText(); + assert.strictEqual(action, 'did queue update when using `new URL()`'); + }); + + it('can assess the request body using string data', async function () { + await completedRequest('#buttonstring'); + const request = await browser.getRequest(0); + assert.equal(request.url, '/telemetry'); + assert.deepEqual(request.body, 'bar'); + const action = await browser.$('#response').getText(); + assert.strictEqual(action, 'did queue update when payload is string'); + }); + + it('can assess the request body using Blob text data', async function () { + await completedRequest('#buttonblobplain'); + const request = await browser.getRequest(0); + assert.equal(request.url, '/telemetry'); + assert.deepEqual(request.body, { foo: 'bar' }); + const action = await browser.$('#response').getText(); + assert.strictEqual( + action, + 'did queue update when payload is "text/plain" Blob', + ); + }); + }); + describe('fetch API', async function () { it('can intercept a simple GET request', async function () { await browser.url('/get.html'); diff --git a/test/utils/express.js b/test/utils/express.js index 069a625b..3e757f4e 100644 --- a/test/utils/express.js +++ b/test/utils/express.js @@ -22,7 +22,11 @@ async function initialize({ baseUrl }) { app.use((req, resp, nextFn) => { if (req.method === 'POST') { // resp.sendFile(`${resources}${req.path}`); - resp.json({ OK: true }); + if (req.url.startsWith('/telemetry')) { + resp.sendStatus(204); + } else { + resp.json({ OK: true }); + } } else { nextFn(); }