diff --git a/src/client/sandbox/event/message.ts b/src/client/sandbox/event/message.ts index 14b914d9c..2b875bb69 100644 --- a/src/client/sandbox/event/message.ts +++ b/src/client/sandbox/event/message.ts @@ -107,14 +107,22 @@ export default class MessageSandbox extends SandboxBase { private _onWindowMessage (e: MessageEvent, originListener): void { const data = MessageSandbox._getMessageData(e); - if (data.type !== MessageType.Service) { + if (data.type === MessageType.Service) + return null; + + if (data.type === MessageType.User) { const originUrl = destLocation.get(); if (data.targetUrl === '*' || destLocation.sameOriginCheck(originUrl, data.targetUrl)) return callEventListener(this.window, originListener, e); + + return null; } - return null; + // NOTE: unwrapped messages arrive when the Hammerhead envelope could not be structured-cloned + // (e.g. Firefox with MessagePort transfers). The browser already validated the target origin + // before delivering the MessageEvent, so it is safe to pass through to the listener. + return callEventListener(this.window, originListener, e); } private static _wrapMessage (type: MessageType, message, targetUrl?: string) { @@ -189,7 +197,10 @@ export default class MessageSandbox extends SandboxBase { const target = nativeMethods.eventTargetGetter.call(this); const data = nativeMethods.messageEventDataGetter.call(this); - if (data && data.type !== MessageType.Service && isWindow(target)) + // NOTE: only unwrap messages that carry the Hammerhead user-message envelope. + // Raw (unwrapped) messages — which may arrive when the envelope could not be + // structured-cloned alongside transferable objects — are returned as-is. + if (data && data.type === MessageType.User && isWindow(target)) return MessageSandbox._getOriginMessageData(data); return data; @@ -215,21 +226,59 @@ export default class MessageSandbox extends SandboxBase { postMessage (contentWindow: Window, args) { const targetUrl = args[1] || destLocation.getOriginHeader(); - // NOTE: We do NOT support the postMessage(message, options) overload. - // The second argument is expected to be `targetOrigin` (string). - // If an options object is provided instead, the call is considered invalid and will be aborted. + // NOTE: postMessage has two overloads: + // 1. postMessage(message, targetOrigin, transfer?) — legacy + // 2. postMessage(message, { targetOrigin, transfer? }) — modern options overload if (typeof targetUrl !== 'string') { - nativeMethods.consoleMeths.log(`testcafe-hammerhead: postMessage called with invalid targetOrigin; aborting call (type: ${typeof targetUrl})`); + if (targetUrl && typeof targetUrl === 'object') + return this._postMessageWithOptionsOverload(contentWindow, args, targetUrl); + return null; } - // NOTE: Here, we pass all messages as "no preference" ("*"). - // We do an origin check in "_onWindowMessage" to access the target origin. - args[1] = '*'; - args[0] = MessageSandbox._wrapMessage(MessageType.User, args[0], targetUrl); + return this._postMessageWrapped(contentWindow, args, targetUrl); + } + + private _postMessageWithOptionsOverload (contentWindow: Window, args, options) { + const resolvedTargetUrl = typeof options.targetOrigin === 'string' + ? options.targetOrigin + : destLocation.getOriginHeader(); + + const originalMessage = args[0]; + + args[0] = MessageSandbox._wrapMessage(MessageType.User, originalMessage, resolvedTargetUrl); + args[1] = nativeMethods.objectAssign({}, options, { targetOrigin: '*' }); + + try { + return fastApply(contentWindow, 'postMessage', args); + } + catch (err) { + args[0] = originalMessage; + args[1] = nativeMethods.objectAssign({}, options, { targetOrigin: '*' }); + + return fastApply(contentWindow, 'postMessage', args); + } + } + private _postMessageWrapped (contentWindow: Window, args, targetUrl: string) { + const originalMessage = args[0]; - return fastApply(contentWindow, 'postMessage', args); + args[1] = '*'; + args[0] = MessageSandbox._wrapMessage(MessageType.User, originalMessage, targetUrl); + + try { + return fastApply(contentWindow, 'postMessage', args); + } + catch (err) { + // NOTE: structured clone may fail when transferable objects (e.g. MessagePort) are + // present in the transfer list. This is observed in Firefox where the Hammerhead + // message envelope breaks the clone+transfer semantics. Fall back to sending the + // original message without wrapping — the receiving side handles unwrapped messages. + args[0] = originalMessage; + args[1] = '*'; + + return fastApply(contentWindow, 'postMessage', args); + } } sendServiceMsg (msg, targetWindow: Window, ports?: Transferable[]) { diff --git a/test/client/fixtures/sandbox/event/message-test.js b/test/client/fixtures/sandbox/event/message-test.js index 6d2e6cc2d..dcf582fbd 100644 --- a/test/client/fixtures/sandbox/event/message-test.js +++ b/test/client/fixtures/sandbox/event/message-test.js @@ -35,20 +35,46 @@ asyncTest('should pass "transfer" argument for "postMessage" (GH-1535)', functio callMethod(window, 'postMessage', ['test', '*', [channel.port1]]); }); -asyncTest('should not accept an object as "targetOrigin"', function () { - var called = false; - var handler = function () { - called = true; +asyncTest('should support postMessage(message, options) overload', function () { + var eventHandlerObject = { + handleEvent: function (e) { + strictEqual(e.data, 'options-overload-test'); + window.removeEventListener('message', eventHandlerObject); + start(); + }, }; - window.addEventListener('message', handler); - callMethod(window, 'postMessage', ['message', { test: 1 }]); + window.addEventListener('message', eventHandlerObject); + callMethod(window, 'postMessage', ['options-overload-test', { targetOrigin: '*' }]); +}); - window.setTimeout(function () { - ok(!called, 'message should not be delivered'); - window.removeEventListener('message', handler); - start(); - }, 100); +asyncTest('should support postMessage(message, options) overload with transfer', function () { + var channel = new MessageChannel(); + + var eventHandlerObject = { + handleEvent: function (e) { + strictEqual(e.data, 'options-transfer-test'); + strictEqual(e.ports.length, 1); + window.removeEventListener('message', eventHandlerObject); + start(); + }, + }; + + window.addEventListener('message', eventHandlerObject); + callMethod(window, 'postMessage', ['options-transfer-test', { targetOrigin: '*', transfer: [channel.port1] }]); +}); + +asyncTest('should deliver message when postMessage is called with an object as second argument', function () { + var handler = function (e) { + if (e.data === 'object-arg-test') { + ok(true, 'message should be delivered via options overload'); + window.removeEventListener('message', handler); + start(); + } + }; + + window.addEventListener('message', handler); + callMethod(window, 'postMessage', ['object-arg-test', { test: 1 }]); }); asyncTest('onmessage event', function () {