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) {
* in the host frame to ensure delivery of "close" notifications from "guest"
* frames.
*
* @param {string} [userAgent] - Test seam
* @return {() => void} - Callback that removes the listener
*/
export function installPortCloseWorkaroundForSafari() {
if (!shouldUseSafariWorkaround()) {
export function installPortCloseWorkaroundForSafari(
userAgent = navigator.userAgent
) {
if (!shouldUseSafariWorkaround(userAgent)) {
return () => {};
}
......@@ -101,11 +104,11 @@ export function installPortCloseWorkaroundForSafari() {
/**
* Test whether this browser needs the workaround for https://bugs.webkit.org/show_bug.cgi?id=231167.
*
* @param {string} userAgent
*/
function shouldUseSafariWorkaround() {
const webkitVersionMatch = navigator.userAgent.match(
/\bAppleWebKit\/([0-9]+)\b/
);
function shouldUseSafariWorkaround(userAgent) {
const webkitVersionMatch = userAgent.match(/\bAppleWebKit\/([0-9]+)\b/);
if (!webkitVersionMatch) {
return false;
}
......@@ -148,7 +151,15 @@ function shouldUseSafariWorkaround() {
* @implements {Destroyable}
*/
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} */
this._port = null;
......@@ -161,7 +172,7 @@ export class PortRPC {
this._callbacks = new Map();
this._listeners = new ListenerCollection();
this._listeners.add(window, 'unload', () => {
this._listeners.add(currentWindow, 'unload', () => {
if (this._port) {
// Send "close" notification directly. This works in Chrome, Firefox and
// Safari >= 16.
......@@ -170,10 +181,15 @@ export class PortRPC {
// 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
// and re-sending the "close" event from there.
if (window !== window.parent && shouldUseSafariWorkaround()) {
window.parent.postMessage({ type: 'hypothesisPortClosed' }, '*', [
this._port,
]);
if (
currentWindow !== currentWindow.parent &&
shouldUseSafariWorkaround(userAgent)
) {
currentWindow.parent.postMessage(
{ type: 'hypothesisPortClosed' },
'*',
[this._port]
);
}
}
});
......
import { PortRPC } from '../port-rpc';
import { PortRPC, installPortCloseWorkaroundForSafari } from '../port-rpc';
describe('PortRPC', () => {
let port1;
......@@ -245,4 +245,88 @@ describe('PortRPC', () => {
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