diff options
Diffstat (limited to 'dom/push/test/test_utils.js')
-rw-r--r-- | dom/push/test/test_utils.js | 245 |
1 files changed, 245 insertions, 0 deletions
diff --git a/dom/push/test/test_utils.js b/dom/push/test/test_utils.js new file mode 100644 index 000000000..efd2f9dd7 --- /dev/null +++ b/dom/push/test/test_utils.js @@ -0,0 +1,245 @@ +(function (g) { + "use strict"; + + let url = SimpleTest.getTestFileURL("mockpushserviceparent.js"); + let chromeScript = SpecialPowers.loadChromeScript(url); + + /** + * Replaces `PushService.jsm` with a mock implementation that handles requests + * from the DOM API. This allows tests to simulate local errors and error + * reporting, bypassing the `PushService.jsm` machinery. + */ + function replacePushService(mockService) { + chromeScript.sendSyncMessage("service-replace"); + chromeScript.addMessageListener("service-delivery-error", function(msg) { + mockService.reportDeliveryError(msg.messageId, msg.reason); + }); + chromeScript.addMessageListener("service-request", function(msg) { + let promise; + try { + let handler = mockService[msg.name]; + promise = Promise.resolve(handler(msg.params)); + } catch (error) { + promise = Promise.reject(error); + } + promise.then(result => { + chromeScript.sendAsyncMessage("service-response", { + id: msg.id, + result: result, + }); + }, error => { + chromeScript.sendAsyncMessage("service-response", { + id: msg.id, + error: error, + }); + }); + }); + } + + function restorePushService() { + chromeScript.sendSyncMessage("service-restore"); + } + + let userAgentID = "8e1c93a9-139b-419c-b200-e715bb1e8ce8"; + + let currentMockSocket = null; + + /** + * Sets up a mock connection for the WebSocket backend. This only replaces + * the transport layer; `PushService.jsm` still handles DOM API requests, + * observes permission changes, writes to IndexedDB, and notifies service + * workers of incoming push messages. + */ + function setupMockPushSocket(mockWebSocket) { + currentMockSocket = mockWebSocket; + currentMockSocket._isActive = true; + chromeScript.sendSyncMessage("socket-setup"); + chromeScript.addMessageListener("socket-client-msg", function(msg) { + mockWebSocket.handleMessage(msg); + }); + } + + function teardownMockPushSocket() { + if (currentMockSocket) { + return new Promise(resolve => { + currentMockSocket._isActive = false; + chromeScript.addMessageListener("socket-server-teardown", resolve); + chromeScript.sendSyncMessage("socket-teardown"); + }); + } + return Promise.resolve(); + } + + /** + * Minimal implementation of web sockets for use in testing. Forwards + * messages to a mock web socket in the parent process that is used + * by the push service. + */ + function MockWebSocket() {} + + let registerCount = 0; + + // Default implementation to make the push server work minimally. + // Override methods to implement custom functionality. + MockWebSocket.prototype = { + // We only allow one active mock web socket to talk to the parent. + // This flag is used to keep track of which mock web socket is active. + _isActive: false, + + onHello(request) { + this.serverSendMsg(JSON.stringify({ + messageType: "hello", + uaid: userAgentID, + status: 200, + use_webpush: true, + })); + }, + + onRegister(request) { + this.serverSendMsg(JSON.stringify({ + messageType: "register", + uaid: userAgentID, + channelID: request.channelID, + status: 200, + pushEndpoint: "https://example.com/endpoint/" + registerCount++ + })); + }, + + onUnregister(request) { + this.serverSendMsg(JSON.stringify({ + messageType: "unregister", + channelID: request.channelID, + status: 200, + })); + }, + + onAck(request) { + // Do nothing. + }, + + handleMessage(msg) { + let request = JSON.parse(msg); + let messageType = request.messageType; + switch (messageType) { + case "hello": + this.onHello(request); + break; + case "register": + this.onRegister(request); + break; + case "unregister": + this.onUnregister(request); + break; + case "ack": + this.onAck(request); + break; + default: + throw new Error("Unexpected message: " + messageType); + } + }, + + serverSendMsg(msg) { + if (this._isActive) { + chromeScript.sendAsyncMessage("socket-server-msg", msg); + } + }, + }; + + g.MockWebSocket = MockWebSocket; + g.setupMockPushSocket = setupMockPushSocket; + g.teardownMockPushSocket = teardownMockPushSocket; + g.replacePushService = replacePushService; + g.restorePushService = restorePushService; +}(this)); + +// Remove permissions and prefs when the test finishes. +SimpleTest.registerCleanupFunction(() => { + return new Promise(resolve => + SpecialPowers.flushPermissions(resolve) + ).then(_ => SpecialPowers.flushPrefEnv()).then(_ => { + restorePushService(); + return teardownMockPushSocket(); + }); +}); + +function setPushPermission(allow) { + return new Promise(resolve => { + SpecialPowers.pushPermissions([ + { type: "desktop-notification", allow, context: document }, + ], resolve); + }); +} + +function setupPrefs() { + return SpecialPowers.pushPrefEnv({"set": [ + ["dom.push.enabled", true], + ["dom.push.connection.enabled", true], + ["dom.push.maxRecentMessageIDsPerSubscription", 0], + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true] + ]}); +} + +function setupPrefsAndReplaceService(mockService) { + replacePushService(mockService); + return setupPrefs(); +} + +function setupPrefsAndMockSocket(mockSocket) { + setupMockPushSocket(mockSocket); + return setupPrefs(); +} + +function injectControlledFrame(target = document.body) { + return new Promise(function(res, rej) { + var iframe = document.createElement("iframe"); + iframe.src = "/tests/dom/push/test/frame.html"; + + var controlledFrame = { + remove() { + target.removeChild(iframe); + iframe = null; + }, + waitOnWorkerMessage(type) { + return iframe ? iframe.contentWindow.waitOnWorkerMessage(type) : + Promise.reject(new Error("Frame removed from document")); + }, + innerWindowId() { + var utils = SpecialPowers.getDOMWindowUtils(iframe.contentWindow); + return utils.currentInnerWindowID; + }, + }; + + iframe.onload = () => res(controlledFrame); + target.appendChild(iframe); + }); +} + +function sendRequestToWorker(request) { + return navigator.serviceWorker.ready.then(registration => { + return new Promise((resolve, reject) => { + var channel = new MessageChannel(); + channel.port1.onmessage = e => { + (e.data.error ? reject : resolve)(e.data); + }; + registration.active.postMessage(request, [channel.port2]); + }); + }); +} + +function waitForActive(swr) { + let sw = swr.installing || swr.waiting || swr.active; + return new Promise(resolve => { + if (sw.state === 'activated') { + resolve(swr); + return; + } + sw.addEventListener('statechange', function onStateChange(evt) { + if (sw.state === 'activated') { + sw.removeEventListener('statechange', onStateChange); + resolve(swr); + } + }); + }); +} |