Commit 99cd82c7 authored by Robert Knight's avatar Robert Knight

Add test for Safari <= 15 workaround PortRPC "close" event

parent 512c9d2c
...@@ -79,10 +79,13 @@ function sendCall(port, method, args = [], sequence = -1) { ...@@ -79,10 +79,13 @@ function sendCall(port, method, args = [], sequence = -1) {
* in the host frame to ensure delivery of "close" notifications from "guest" * in the host frame to ensure delivery of "close" notifications from "guest"
* frames. * frames.
* *
* @param {string} [userAgent] - Test seam
* @return {() => void} - Callback that removes the listener * @return {() => void} - Callback that removes the listener
*/ */
export function installPortCloseWorkaroundForSafari() { export function installPortCloseWorkaroundForSafari(
if (!shouldUseSafariWorkaround()) { userAgent = navigator.userAgent
) {
if (!shouldUseSafariWorkaround(userAgent)) {
return () => {}; return () => {};
} }
...@@ -101,11 +104,11 @@ export function installPortCloseWorkaroundForSafari() { ...@@ -101,11 +104,11 @@ export function installPortCloseWorkaroundForSafari() {
/** /**
* Test whether this browser needs the workaround for https://bugs.webkit.org/show_bug.cgi?id=231167. * Test whether this browser needs the workaround for https://bugs.webkit.org/show_bug.cgi?id=231167.
*
* @param {string} userAgent
*/ */
function shouldUseSafariWorkaround() { function shouldUseSafariWorkaround(userAgent) {
const webkitVersionMatch = navigator.userAgent.match( const webkitVersionMatch = userAgent.match(/\bAppleWebKit\/([0-9]+)\b/);
/\bAppleWebKit\/([0-9]+)\b/
);
if (!webkitVersionMatch) { if (!webkitVersionMatch) {
return false; return false;
} }
...@@ -148,7 +151,15 @@ function shouldUseSafariWorkaround() { ...@@ -148,7 +151,15 @@ function shouldUseSafariWorkaround() {
* @implements {Destroyable} * @implements {Destroyable}
*/ */
export class PortRPC { export class PortRPC {
constructor() { /**
* @param {object} options
* @param {string} [options.userAgent] - Test seam
* @param {Window} [options.currentWindow] - Test seam
*/
constructor({
userAgent = navigator.userAgent,
currentWindow = window,
} = {}) {
/** @type {MessagePort|null} */ /** @type {MessagePort|null} */
this._port = null; this._port = null;
...@@ -161,7 +172,7 @@ export class PortRPC { ...@@ -161,7 +172,7 @@ export class PortRPC {
this._callbacks = new Map(); this._callbacks = new Map();
this._listeners = new ListenerCollection(); this._listeners = new ListenerCollection();
this._listeners.add(window, 'unload', () => { this._listeners.add(currentWindow, 'unload', () => {
if (this._port) { if (this._port) {
// Send "close" notification directly. This works in Chrome, Firefox and // Send "close" notification directly. This works in Chrome, Firefox and
// Safari >= 16. // Safari >= 16.
...@@ -170,10 +181,15 @@ export class PortRPC { ...@@ -170,10 +181,15 @@ export class PortRPC {
// To work around a bug in Safari <= 15 which prevents sending messages // To work around a bug in Safari <= 15 which prevents sending messages
// while a window is unloading, try transferring the port to the parent frame // while a window is unloading, try transferring the port to the parent frame
// and re-sending the "close" event from there. // and re-sending the "close" event from there.
if (window !== window.parent && shouldUseSafariWorkaround()) { if (
window.parent.postMessage({ type: 'hypothesisPortClosed' }, '*', [ currentWindow !== currentWindow.parent &&
this._port, shouldUseSafariWorkaround(userAgent)
]); ) {
currentWindow.parent.postMessage(
{ type: 'hypothesisPortClosed' },
'*',
[this._port]
);
} }
} }
}); });
......
import { PortRPC } from '../port-rpc'; import { PortRPC, installPortCloseWorkaroundForSafari } from '../port-rpc';
describe('PortRPC', () => { describe('PortRPC', () => {
let port1; let port1;
...@@ -245,4 +245,88 @@ describe('PortRPC', () => { ...@@ -245,4 +245,88 @@ describe('PortRPC', () => {
assert.calledOnce(closeHandler); assert.calledOnce(closeHandler);
}); });
/** Transfer a MessagePort to another frame and return the transferred port. */
async function transferPort(port, targetWindow) {
const transferredPort = new Promise(resolve => {
targetWindow.addEventListener('message', e => {
if (e.ports[0]) {
resolve(e.ports[0]);
}
});
});
targetWindow.postMessage({}, '*', [port]);
return transferredPort;
}
describe('Safari <= 15 workaround', () => {
const safariUserAgent =
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.2 Safari/605.1.15';
let removeWorkaround;
let childFrame;
beforeEach(() => {
childFrame = document.createElement('iframe');
document.body.append(childFrame);
removeWorkaround = installPortCloseWorkaroundForSafari(safariUserAgent);
});
afterEach(() => {
removeWorkaround();
childFrame.remove();
});
it('transfers port to parent frame and sends "close" event from there when window is unloaded', async () => {
const { port1, port2 } = new MessageChannel();
const transferredPort = await transferPort(
port1,
childFrame.contentWindow
);
const sender = new PortRPC({
userAgent: safariUserAgent,
currentWindow: childFrame.contentWindow,
});
const receiver = new PortRPC();
const closeHandler = sinon.stub();
receiver.on('close', closeHandler);
receiver.connect(port2);
sender.connect(transferredPort);
await waitForMessageDelivery();
closeHandler.resetHistory();
// Emulate the Safari bug by disabling `postMessage` on the sending port
// in the original frame. When the port is transferred to the "parent"
// frame, it will reconstituted as a new MessagePort instance.
transferredPort.postMessage = () => {};
// Unload the child frame. The "unload" handler will transfer the port to
// the parent frame and the "close" event will be sent from there.
childFrame.remove();
await waitForMessageDelivery();
assert.called(closeHandler);
});
});
[
// Chrome 100
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4867.0 Safari/537.36',
// Firefox 96
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:96.0) Gecko/20100101 Firefox/96.0',
].forEach(userAgent => {
it('does not use workaround in unaffected browsers', () => {
sinon.stub(window, 'addEventListener');
const removeWorkaround = installPortCloseWorkaroundForSafari(userAgent);
removeWorkaround();
assert.notCalled(window.addEventListener);
window.addEventListener.restore();
});
});
}); });
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment