Commit 00c59e0e authored by Eduardo Sanz García's avatar Eduardo Sanz García Committed by Eduardo

Apply suggestions from code review

Co-authored-by: 's avatarRobert Knight <robertknight@gmail.com>
parent e78ada77
...@@ -2,7 +2,7 @@ import { ListenerCollection } from './listener-collection'; ...@@ -2,7 +2,7 @@ import { ListenerCollection } from './listener-collection';
import { isMessageEqual, SOURCE as source } from './port-util'; import { isMessageEqual, SOURCE as source } from './port-util';
const MAX_WAIT_FOR_PORT = 1000 * 30; const MAX_WAIT_FOR_PORT = 1000 * 30;
const POLLING_INTERVAL_FOR_PORT = 500; const POLLING_INTERVAL_FOR_PORT = 250;
/** /**
* @typedef {import('../types/annotator').Destroyable} Destroyable * @typedef {import('../types/annotator').Destroyable} Destroyable
...@@ -12,8 +12,7 @@ const POLLING_INTERVAL_FOR_PORT = 500; ...@@ -12,8 +12,7 @@ const POLLING_INTERVAL_FOR_PORT = 500;
*/ */
/** /**
* PortFinder class should be used in frames that are not the `host` frame. It * PortFinder helps to discover `MessagePort` on a specific channel.
* helps to discover `MessagePort` on a specific channel.
* *
* Channel nomenclature is `[frame1]-[frame2]` so that: * Channel nomenclature is `[frame1]-[frame2]` so that:
* - `port1` should be owned by/transferred to `frame1`, and * - `port1` should be owned by/transferred to `frame1`, and
...@@ -28,118 +27,65 @@ export class PortFinder { ...@@ -28,118 +27,65 @@ export class PortFinder {
this._listeners = new ListenerCollection(); this._listeners = new ListenerCollection();
} }
// Two important characteristics of `MessagePort`: destroy() {
// - it can only be used by one frame; the port is neutered if, after started to this._listeners.removeAll();
// be used to receive messages, the port is transferred to a different frame. }
// - messages are queued until the other port is ready to listen (`port.start()`)
/** /**
* `guest-host` communication * Polls the hostFrame for a specific port and returns a Promise of the port.
* @typedef {{channel: 'guest-host', hostFrame: Window, port: 'guest'}} options0
*
* `guest-sidebar` communication
* @typedef {{channel: 'guest-sidebar', hostFrame: Window, port: 'guest'}} options1
* *
* `host-sidebar` communication * @param {object} options
* @typedef {{channel: 'host-sidebar', hostFrame: Window, port: 'sidebar'}} options2 * @param {Channel} options.channel - requested channel
* * @param {Window} options.hostFrame - frame where the hypothesis client is
* `notebook-sidebar` communication * loaded and `PortProvider` is listening for messages
* @typedef {{channel: 'notebook-sidebar', hostFrame: Window, port: 'notebook'}} options3 * @param {Port} options.port - requested port
*
* @param {options0|options1|options2|options3} options
* @return {Promise<MessagePort>} * @return {Promise<MessagePort>}
*/ */
discover(options) { discover({ channel, hostFrame, port }) {
const { channel, port } = options; let isValidRequest = false;
if (
(channel === 'guest-host' && port === 'guest') ||
(channel === 'guest-sidebar' && port === 'guest') ||
(channel === 'host-sidebar' && port === 'sidebar') ||
(channel === 'notebook-sidebar' && port === 'notebook')
) {
isValidRequest = true;
}
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if ( if (!isValidRequest) {
(channel === 'guest-host' && port === 'guest') || reject(new Error('Invalid request of channel/port'));
(channel === 'guest-sidebar' && port === 'guest') ||
(channel === 'host-sidebar' && port === 'sidebar') ||
(channel === 'notebook-sidebar' && port === 'notebook')
) {
this._requestPort({
...options,
reject,
resolve,
});
return; return;
} }
reject(new Error('Invalid request of channel/port')); function postRequest() {
}); hostFrame.postMessage({ channel, port, source, type: 'request' }, '*');
} }
/**
* @typedef RequestPortOptions
* @prop {Channel} channel - requested channel
* @prop {Window} hostFrame - the frame where the hypothesis client is loaded.
* It is used to send a `window.postMessage`.
* @prop {Port} port - requested port
* @prop {(reason: Error) => void} reject - execute the `Promise.reject` in case
* the `host` frame takes too long to answer the request.
* @prop {(port: MessagePort) => void} resolve - execute the `Promise.resolve`
* when `host` frame successfully answers the request.
*/
/**
* Register a listener for the port `offer` and sends a request for one port.
*
* @param {RequestPortOptions} options
*/
_requestPort({ channel, hostFrame, port, reject, resolve }) {
function postRequest() {
hostFrame.postMessage({ channel, port, source, type: 'request' }, '*');
}
const intervalId = window.setInterval(
() => postRequest(),
POLLING_INTERVAL_FOR_PORT
);
// The `host` frame maybe busy, that's why we should wait. const intervalId = setInterval(
const timeoutId = window.setTimeout(() => { () => postRequest(),
clearInterval(intervalId); POLLING_INTERVAL_FOR_PORT
reject(
new Error(`Unable to find '${port}' port on '${channel}' channel`)
); );
}, MAX_WAIT_FOR_PORT);
// TODO: It would be nice to remove the listener after receiving the port.
this._listeners.add(window, 'message', event =>
this._handlePortOffer(/** @type {MessageEvent} */ (event), {
intervalId,
message: { channel, port, source, type: 'offer' },
resolve,
timeoutId,
})
);
postRequest(); // The `host` frame maybe busy, that's why we should wait.
} const timeoutId = window.setTimeout(() => {
clearInterval(intervalId);
reject(
new Error(`Unable to find '${port}' port on '${channel}' channel`)
);
}, MAX_WAIT_FOR_PORT);
/** // TODO: It would be nice to remove the listener after receiving the port.
* Resolve with a MessagePort when the `offer` message matches. this._listeners.add(window, 'message', event => {
* const { data, ports } = /** @type {MessageEvent} */ (event);
* @param {MessageEvent} event if (isMessageEqual(data, { channel, port, source, type: 'offer' })) {
* @param {object} options clearInterval(intervalId);
* @param {Message} options.message clearTimeout(timeoutId);
* @param {(port: MessagePort) => void} options.resolve resolve(ports[0]);
* @param {number} options.timeoutId }
* @param {number} [options.intervalId] });
*/
_handlePortOffer(
{ data, ports },
{ message, resolve, timeoutId, intervalId }
) {
if (isMessageEqual(data, message)) {
clearInterval(intervalId);
clearTimeout(timeoutId);
resolve(ports[0]);
}
}
destroy() { postRequest();
this._listeners.removeAll(); });
} }
} }
import { TinyEmitter } from 'tiny-emitter';
import { ListenerCollection } from './listener-collection'; import { ListenerCollection } from './listener-collection';
import { isMessageEqual, SOURCE as source } from './port-util'; import { isMessageEqual, SOURCE as source } from './port-util';
...@@ -38,7 +40,7 @@ import { isMessageEqual, SOURCE as source } from './port-util'; ...@@ -38,7 +40,7 @@ import { isMessageEqual, SOURCE as source } from './port-util';
* - `host-sidebar` * - `host-sidebar`
* - `notebook-sidebar` * - `notebook-sidebar`
* *
* `PortProvider` is used only on the `host` frame. The rest of the frames use the * `PortProvider` is used only in the `host` frame. The other frames use the
* companion class, `PortFinder`. `PortProvider` creates a `MessageChannel` * companion class, `PortFinder`. `PortProvider` creates a `MessageChannel`
* for two frames to communicate with each other. It also listens to requests for * for two frames to communicate with each other. It also listens to requests for
* particular `MessagePort` and dispatches the corresponding `MessagePort`. * particular `MessagePort` and dispatches the corresponding `MessagePort`.
...@@ -55,7 +57,14 @@ import { isMessageEqual, SOURCE as source } from './port-util'; ...@@ -55,7 +57,14 @@ import { isMessageEqual, SOURCE as source } from './port-util';
* V * V
* 5. send reciprocal port to the `sidebar` frame using the `host-sidebar` * 5. send reciprocal port to the `sidebar` frame using the `host-sidebar`
* *
* Channel nomenclature is `[frame1]-[frame2]` so that:
* - `port1` should be owned by/transferred to `frame1`, and
* - `port2` should be owned by/transferred to `frame2`
* *
* @implements Destroyable
*/
/*
* In some situations, because `guest` iframe/s load in parallel to the `host` * In some situations, because `guest` iframe/s load in parallel to the `host`
* frame, we can not assume that the code in the `host` frame is executed before * frame, we can not assume that the code in the `host` frame is executed before
* the code in a `guest` frame. Hence, we can't assume that `PortProvider` (in * the code in a `guest` frame. Hence, we can't assume that `PortProvider` (in
...@@ -63,23 +72,27 @@ import { isMessageEqual, SOURCE as source } from './port-util'; ...@@ -63,23 +72,27 @@ import { isMessageEqual, SOURCE as source } from './port-util';
* Therefore, for the `PortFinder`, we implement a polling strategy (sending a * Therefore, for the `PortFinder`, we implement a polling strategy (sending a
* message every N milliseconds) until a response is received. * message every N milliseconds) until a response is received.
* *
* Channel nomenclature is `[frame1]-[frame2]` so that: * Two important characteristics of `MessagePort`:
* - `port1` should be owned by/transferred to `frame1`, and * - it can only be used by one frame: in Chrome the port is neutered if transferred twice
* - `port2` should be owned by/transferred to `frame2` * - messages are queued until the other port is ready to listen (`port.start()`)
*
* @implements Destroyable
*/ */
export class PortProvider { export class PortProvider {
/** /**
* @param {string} hypothesisAppsURL * @param {string} hypothesisAppsOrigin - the origin of the hypothesis apps
* is use to send the notebook and sidebar ports to only the frames that
* matches the origin.
*/ */
constructor(hypothesisAppsURL) { constructor(hypothesisAppsOrigin) {
this._hypothesisAppsOrigin = new URL(hypothesisAppsURL).origin; this._hypothesisAppsOrigin = hypothesisAppsOrigin;
this._emitter = new TinyEmitter();
// Although some channels (v.gr. `notebook-sidebar`) have only one // Although some channels (v.gr. `notebook-sidebar`) have only one
// `MessageChannel`, other channels (v.gr. `guest-sidebar`) can have multiple // `MessageChannel`, other channels (v.gr. `guest-sidebar`) can have multiple
// `MessageChannel`s. In spite of the number channel, we store all // `MessageChannel`s. In spite of the number channel, we store all
// `MessageChannel` on a `Map<Window, MessageChannel>`. // `MessageChannel` on a `Map<Window, MessageChannel>`. The `Window` refers
// to the frame that sends the initial request that triggers creation of a
// channel.
/** @type {Map<Channel, Map<Window, MessageChannel>>} */ /** @type {Map<Channel, Map<Window, MessageChannel>>} */
this._channels = new Map(); this._channels = new Map();
...@@ -88,90 +101,19 @@ export class PortProvider { ...@@ -88,90 +101,19 @@ export class PortProvider {
this._hostSidebarChannel = new MessageChannel(); this._hostSidebarChannel = new MessageChannel();
this._listeners = new ListenerCollection(); this._listeners = new ListenerCollection();
this._listen();
}
/**
* @param {'onHostPortRequest'} _eventName
* @param {(MessagePort, channel: 'guest') => void} handler - this handler
* fires when a request for the 'guest-host' channel is listened.
*/
addEventListener(_eventName, handler) {
this._onHostPortRequest = handler;
}
/**
* Returns a port from a channel. Currently, only returns the `host` port from
* the `host-Sidebar` channel. Otherwise, it returns `null`.
*
* @param {object} options
* @param {'host-sidebar'} options.channel
* @param {'host'} options.port
*/
getPort({ channel, port }) {
if (channel === 'host-sidebar' && port === 'host') {
return this._hostSidebarChannel.port1;
}
return null;
}
/**
* Initiate the listener of port requests by other frames.
*/
_listen() {
/** @type {Array<{allowedOrigin: string, channel: Channel, port: Port}>} */
([
{
allowedOrigin: '*',
channel: 'guest-host',
port: 'guest',
},
{
allowedOrigin: '*',
channel: 'guest-sidebar',
port: 'guest',
},
{
allowedOrigin: this._hypothesisAppsOrigin,
channel: 'host-sidebar',
port: 'sidebar',
},
{
allowedOrigin: this._hypothesisAppsOrigin,
channel: 'notebook-sidebar',
port: 'notebook',
},
]).forEach(({ allowedOrigin, channel, port }) => {
this._listeners.add(window, 'message', event =>
this._handlePortRequest(/** @type {MessageEvent} */ (event), {
allowedMessage: {
channel,
port,
source,
type: 'request',
},
allowedOrigin,
})
);
});
} }
/**
* @typedef Options
* @prop {Message} allowedMessage - the request `MessageEvent` must match this
* object to grant the port.
* @prop {string} allowedOrigin - the origin in the `MessageEvent` must match
* this value to grant the port. If '*' allow all origins.
*/
/** /**
* Checks the `postMessage` origin and message. * Checks the `postMessage` origin and message.
* *
* @param {MessageEvent} event * @param {MessageEvent} event
* @param {Options} options * @param {Message} allowedMessage - the MessageEvent's data must match this
* object to grant the port.
* @param {string} allowedOrigin - the MessageEvent's origin must match this
* value to grant the port. If '*' allow all origins.
*/ */
_isValidRequest({ data, origin, source }, { allowedMessage, allowedOrigin }) { _isValidRequest(event, allowedMessage, allowedOrigin) {
const { data, origin, source } = event;
if (allowedOrigin !== '*' && origin !== allowedOrigin) { if (allowedOrigin !== '*' && origin !== allowedOrigin) {
return false; return false;
} }
...@@ -195,16 +137,17 @@ export class PortProvider { ...@@ -195,16 +137,17 @@ export class PortProvider {
} }
/** /**
* Send (1) the requested port via `frame#postMessage` (the origin is set * Send a message and a port to the corresponding destinations.
* to match the allowedOrigin) and (2) the reciprocal port, if one is provided,
* to the `sidebar` frame using `host-sidebar(channel).host(port)#postMessage`
* *
* @param {MessageEvent} event * @param {MessageEvent} event
* @param {Options & {port: MessagePort, reciprocalPort? : MessagePort}} options * @param {Message} message - the message to be sent.
* @param {MessagePort} port - the port to be sent via `window#postMessage`
* (the origin is set to match the MessageEvent's origin)frame that sends the initial request th
* @param {MessagePort} [reciprocalPort] - if a reciprocal port is provided,
* send this port (1) to the `sidebar` frame using the `host-sidebar`
* channel or (2) through the `onHostPortRequest` event listener.
*/ */
_sendPort(event, { allowedMessage, port, reciprocalPort }) { _sendPort(event, message, port, reciprocalPort) {
const message = { ...allowedMessage, type: 'offer' };
const source = /** @type {Window} */ (event.source); const source = /** @type {Window} */ (event.source);
source.postMessage(message, event.origin, [port]); source.postMessage(message, event.origin, [port]);
...@@ -214,54 +157,110 @@ export class PortProvider { ...@@ -214,54 +157,110 @@ export class PortProvider {
this._hostSidebarChannel.port1.postMessage(message, [reciprocalPort]); this._hostSidebarChannel.port1.postMessage(message, [reciprocalPort]);
} }
if (message.channel === 'guest-host' && message.port === 'guest') { if (message.channel === 'guest-host' && message.port === 'guest') {
this._onHostPortRequest?.(reciprocalPort, message.port); this._emitter.emit('hostPortRequest', reciprocalPort, message.port);
} }
} }
} }
/** /**
* Respond to request of ports on channels. * @param {'hostPortRequest'} eventName
* @param {MessageEvent} event * @param {(MessagePort, channel: 'guest') => void} handler - this handler
* @param {Options} options * fires when a request for the 'guest-host' channel is listened.
*/ */
_handlePortRequest(event, options) { on(eventName, handler) {
if (!this._isValidRequest(event, options)) { this._emitter.on(eventName, handler);
return; }
/**
* Returns a port from a channel. Currently, only returns the `host` port from
* the `host-Sidebar` channel. Otherwise, it returns `null`.
*
* @param {object} options
* @param {'host-sidebar'} options.channel
* @param {'host'} options.port
*/
getPort({ channel, port }) {
if (channel === 'host-sidebar' && port === 'host') {
return this._hostSidebarChannel.port1;
} }
const { channel } = options.allowedMessage; return null;
}
let windowChannelMap = this._channels.get(channel); /**
if (!windowChannelMap) { * Initiate the listener of port requests by other frames.
windowChannelMap = new Map(); */
this._channels.set(channel, windowChannelMap); listen() {
} this._listeners.add(window, 'message', messageEvent => {
const event = /** @type {MessageEvent} */ (messageEvent);
/** @type {Array<{allowedOrigin: string, channel: Channel, port: Port}>} */
([
{
allowedOrigin: '*',
channel: 'guest-host',
port: 'guest',
},
{
allowedOrigin: '*',
channel: 'guest-sidebar',
port: 'guest',
},
{
allowedOrigin: this._hypothesisAppsOrigin,
channel: 'host-sidebar',
port: 'sidebar',
},
{
allowedOrigin: this._hypothesisAppsOrigin,
channel: 'notebook-sidebar',
port: 'notebook',
},
]).forEach(({ allowedOrigin, channel, port }) => {
/** @type {Message} */
const allowedMessage = {
channel,
port,
source,
type: 'request',
};
const source = /** @type {Window} */ (event.source); if (!this._isValidRequest(event, allowedMessage, allowedOrigin)) {
let messageChannel = windowChannelMap.get(source); return;
}
// Ignore the port request if the channel for the specified window has let windowChannelMap = this._channels.get(channel);
// already been created. This is to avoid transfer the port more than once. if (!windowChannelMap) {
if (messageChannel) { windowChannelMap = new Map();
return; this._channels.set(channel, windowChannelMap);
} }
// `host-sidebar` channel is an special case, because it is created in the const eventSource = /** @type {Window} */ (event.source);
// constructor. let messageChannel = windowChannelMap.get(eventSource);
if (channel === 'host-sidebar') {
windowChannelMap.set(source, this._hostSidebarChannel); // Ignore the port request if the channel for the specified window has
this._sendPort(event, { // already been created. This is to avoid transfer the port more than once.
...options, if (messageChannel) {
port: this._hostSidebarChannel.port2, return;
}); }
return;
}
messageChannel = new MessageChannel(); /** @type {Message} */
windowChannelMap.set(source, messageChannel); const message = { ...allowedMessage, type: 'offer' };
const { port1, port2 } = messageChannel; // `host-sidebar` channel is an special case, because it is created in the
this._sendPort(event, { ...options, port: port1, reciprocalPort: port2 }); // constructor.
if (channel === 'host-sidebar') {
windowChannelMap.set(eventSource, this._hostSidebarChannel);
this._sendPort(event, message, this._hostSidebarChannel.port2);
return;
}
messageChannel = new MessageChannel();
windowChannelMap.set(eventSource, messageChannel);
const { port1, port2 } = messageChannel;
this._sendPort(event, message, port1, port2);
});
});
} }
destroy() { destroy() {
......
// Because there are many `postMessages` on the `host` frame, the SOURCE property // Because there are many `postMessages` on the `host` frame, the SOURCE property
// is added to the hypothesis `postMessages` to identify the provenance of the // is added to the hypothesis `postMessages` to identify the provenance of the
// message and avoid listening to messages that could have the same properties // message and avoid listening to messages that could have the same properties
// but different source. This is not a is not a security feature but an // but different source. This is not a security feature but an
// anti-collision mechanism. // anti-collision mechanism.
export const SOURCE = 'hypothesis'; export const SOURCE = 'hypothesis';
/** /**
* These types are the used in by `PortProvider` and `PortFinder` for the
* inter-frame discovery and communication processes.
* @typedef {'guest-host'|'guest-sidebar'|'host-sidebar'|'notebook-sidebar'} Channel * @typedef {'guest-host'|'guest-sidebar'|'host-sidebar'|'notebook-sidebar'} Channel
* @typedef {'guest'|'host'|'notebook'|'sidebar'} Port * @typedef {'guest'|'host'|'notebook'|'sidebar'} Port
* *
......
...@@ -26,92 +26,93 @@ describe('PortFinder', () => { ...@@ -26,92 +26,93 @@ describe('PortFinder', () => {
portFinder.destroy(); portFinder.destroy();
}); });
[ describe('#destroy', () => {
{ channel: 'invalid', port: 'guest' }, it('ignores `offer` messages of ports', async () => {
{ channel: 'guest-host', port: 'invalid' },
].forEach(({ channel, port }) =>
it('rejects if requesting an invalid port', async () => {
let error; let error;
const channel = 'host-sidebar';
const port = 'sidebar';
const { port1 } = new MessageChannel();
const clock = sinon.useFakeTimers();
try { try {
await portFinder.discover({ portFinder
channel, .discover({
hostFrame: window, channel,
port, hostFrame: window,
port,
})
.catch(e => (error = e));
portFinder.destroy();
sendMessage({
data: { channel, port, source, type: 'offer' },
ports: [port1],
}); });
} catch (e) { clock.tick(30000);
error = e; } finally {
clock.restore();
} }
assert.equal(error.message, 'Invalid request of channel/port');
})
);
[
{ channel: 'guest-host', port: 'guest' },
{ channel: 'guest-sidebar', port: 'guest' },
{ channel: 'host-sidebar', port: 'sidebar' },
{ channel: 'notebook-sidebar', port: 'notebook' },
].forEach(({ channel, port }) =>
it('resolves if requesting a valid port', async () => {
const { port1 } = new MessageChannel();
let resolvedPort;
portFinder
.discover({
channel,
hostFrame: window,
port,
})
.then(port => (resolvedPort = port));
sendMessage({
data: { channel, port, source, type: 'offer' },
ports: [port1],
});
await delay(0); await delay(0);
assert.instanceOf(resolvedPort, MessagePort); assert.equal(
}) error.message,
); "Unable to find 'sidebar' port on 'host-sidebar' channel"
);
it("timeouts if host doesn't respond", async () => { });
let error; });
const channel = 'host-sidebar';
const port = 'sidebar'; describe('#discover', () => {
const clock = sinon.useFakeTimers(); [
{ channel: 'invalid', port: 'guest' },
try { { channel: 'guest-host', port: 'invalid' },
portFinder { channel: 'guest-host', port: 'host' },
.discover({ ].forEach(({ channel, port }) =>
channel, it('rejects if requesting an invalid port', async () => {
hostFrame: window, let error;
port, try {
}) await portFinder.discover({
.catch(e => (error = e)); channel,
clock.tick(30000); hostFrame: window,
} finally { port,
clock.restore(); });
} } catch (e) {
error = e;
assert.callCount(window.postMessage, 61); }
assert.alwaysCalledWithExactly( assert.equal(error.message, 'Invalid request of channel/port');
window.postMessage, })
{ channel, port, source, type: 'request' },
'*'
); );
await delay(0); [
{ channel: 'guest-host', port: 'guest' },
{ channel: 'guest-sidebar', port: 'guest' },
{ channel: 'host-sidebar', port: 'sidebar' },
{ channel: 'notebook-sidebar', port: 'notebook' },
].forEach(({ channel, port }) =>
it('resolves if requesting a valid port', async () => {
const { port1 } = new MessageChannel();
let resolvedPort;
assert.equal( portFinder
error.message, .discover({
"Unable to find 'sidebar' port on 'host-sidebar' channel" channel,
hostFrame: window,
port,
})
.then(port => (resolvedPort = port));
sendMessage({
data: { channel, port, source, type: 'offer' },
ports: [port1],
});
await delay(0);
assert.instanceOf(resolvedPort, MessagePort);
})
); );
});
describe('#destroy', () => { it("timeouts if host doesn't respond", async () => {
it('ignores `offer` messages of ports', async () => {
let error; let error;
const channel = 'host-sidebar'; const channel = 'host-sidebar';
const port = 'sidebar'; const port = 'sidebar';
const { port1 } = new MessageChannel();
const clock = sinon.useFakeTimers(); const clock = sinon.useFakeTimers();
try { try {
...@@ -122,16 +123,18 @@ describe('PortFinder', () => { ...@@ -122,16 +123,18 @@ describe('PortFinder', () => {
port, port,
}) })
.catch(e => (error = e)); .catch(e => (error = e));
portFinder.destroy();
sendMessage({
data: { channel, port, source, type: 'offer' },
ports: [port1],
});
clock.tick(30000); clock.tick(30000);
} finally { } finally {
clock.restore(); clock.restore();
} }
assert.callCount(window.postMessage, 121);
assert.alwaysCalledWithExactly(
window.postMessage,
{ channel, port, source, type: 'request' },
'*'
);
await delay(0); await delay(0);
assert.equal( assert.equal(
......
...@@ -23,7 +23,8 @@ describe('PortProvider', () => { ...@@ -23,7 +23,8 @@ describe('PortProvider', () => {
beforeEach(() => { beforeEach(() => {
sinon.stub(window, 'postMessage'); sinon.stub(window, 'postMessage');
portProvider = new PortProvider(window.location.href); portProvider = new PortProvider(window.location.origin);
portProvider.listen();
}); });
afterEach(() => { afterEach(() => {
...@@ -31,6 +32,23 @@ describe('PortProvider', () => { ...@@ -31,6 +32,23 @@ describe('PortProvider', () => {
portProvider.destroy(); portProvider.destroy();
}); });
describe('#destroy', () => {
it('ignores valid port request if `PortFinder` has been destroyed', async () => {
portProvider.destroy();
sendMessage({
data: {
channel: 'host-sidebar',
port: 'sidebar',
source,
type: 'request',
},
});
await delay(0);
assert.notCalled(window.postMessage);
});
});
describe('#getPort', () => { describe('#getPort', () => {
it('returns `null` if called with wrong arguments', () => { it('returns `null` if called with wrong arguments', () => {
let hostPort; let hostPort;
...@@ -59,20 +77,30 @@ describe('PortProvider', () => { ...@@ -59,20 +77,30 @@ describe('PortProvider', () => {
}); });
}); });
describe('#destroy', () => { describe('#listen', () => {
it('ignores valid port request if `PortFinder` has been destroyed', async () => { it('ignores all port request until start listening', async () => {
portProvider.destroy(); portProvider.destroy();
portProvider = new PortProvider(window.location.origin);
const data = {
channel: 'host-sidebar',
port: 'sidebar',
source,
type: 'request',
};
sendMessage({ sendMessage({
data: { data,
channel: 'host-sidebar',
port: 'sidebar',
source,
type: 'request',
},
}); });
await delay(0); await delay(0);
assert.notCalled(window.postMessage); assert.notCalled(window.postMessage);
portProvider.listen();
sendMessage({
data,
});
await delay(0);
assert.calledOnce(window.postMessage);
}); });
}); });
...@@ -94,7 +122,6 @@ describe('PortProvider', () => { ...@@ -94,7 +122,6 @@ describe('PortProvider', () => {
data, data,
source: new MessageChannel().port1, source: new MessageChannel().port1,
}); });
await delay(0); await delay(0);
assert.notCalled(window.postMessage); assert.notCalled(window.postMessage);
...@@ -132,7 +159,6 @@ describe('PortProvider', () => { ...@@ -132,7 +159,6 @@ describe('PortProvider', () => {
data, data,
origin: 'https://dummy.com', origin: 'https://dummy.com',
}); });
await delay(0); await delay(0);
assert.notCalled(window.postMessage); assert.notCalled(window.postMessage);
...@@ -148,7 +174,6 @@ describe('PortProvider', () => { ...@@ -148,7 +174,6 @@ describe('PortProvider', () => {
sendMessage({ sendMessage({
data, data,
}); });
await delay(0); await delay(0);
assert.calledWith( assert.calledWith(
...@@ -217,7 +242,7 @@ describe('PortProvider', () => { ...@@ -217,7 +242,7 @@ describe('PortProvider', () => {
it('sends the reciprocal port of the `guest-host` channel (via listener)', async () => { it('sends the reciprocal port of the `guest-host` channel (via listener)', async () => {
const handler = sinon.stub(); const handler = sinon.stub();
portProvider.addEventListener('onHostPortRequest', handler); portProvider.on('hostPortRequest', handler);
const data = { const data = {
channel: 'guest-host', channel: 'guest-host',
port: 'guest', port: 'guest',
......
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