Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions lib/interceptor.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -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 () {
Expand Down Expand Up @@ -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;
Expand Down
File renamed without changes.
44 changes: 44 additions & 0 deletions test/site/send_beacon.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>sendBeacon request</title>
</head>
<body>
<p id="text">This file makes various sendBeacon requests</p>
<button id="url">Press me for a URL() argument</button>
<button id="buttonstring">Press me for string data</button>
<button id="buttonblobplain">Press me for stringified JSON data as Blob</button>
<div id="response"></div>
<script>
'use strict';

(function (window, document) {
const resp = document.querySelector('#response');

function log(queued, when) {
resp.textContent += `${queued === true ? "did" : "did not"} queue update ${when}\n`;
}

document.querySelector('#url').addEventListener('click', function () {
const queued = window.navigator.sendBeacon(new URL('/telemetry?data=querystring', window.location.origin));
log(queued, 'when using `new URL()`');
});

document.querySelector('#buttonstring').addEventListener('click', function () {
const data = 'bar';
const isQueuedForDelivery = window.navigator.sendBeacon('/telemetry', data);
log(isQueuedForDelivery, "when payload is string");
});

document.querySelector('#buttonblobplain').addEventListener('click', function () {
const data = new Blob([JSON.stringify({ foo: 'bar' })], { type: 'text/plain' });
const isQueuedForDelivery = window.navigator.sendBeacon('/telemetry', data);
log(isQueuedForDelivery, 'when payload is "text/plain" Blob')
});

})(window, window.document);
</script>
</body>
</html>
50 changes: 49 additions & 1 deletion test/spec/plugin_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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');
Expand Down
6 changes: 5 additions & 1 deletion test/utils/express.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down