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

Improve ergonomics of PortFinder and PortProvider

For PortFinder, I followed this advice:
https://github.com/hypothesis/client/pull/3881#discussion_r743030606,
except that I made the argument of `PorFinder#discover` and string
(instead of an object).

```
const portFinder = new PortFinder({ source: 'guest', hostFrame });
portFinder.discover('sidebar');
portFinder.discover('host');
```

For PortProvider, I followed this advice:
https://github.com/hypothesis/client/pull/3881#discussion_r743033013,
except that I used a getter.

```
cont portProvider = new PortProvider(...)
const bridge = new Bridge();
bridge.createChannel(portProvider.sidebarPort);
```

I have renamed the properties of `Message`:
- `source` becomes `authority`
- `channel` and `port` have been replaced by `frame1` and `frame2`

I did that to align `port1` and `frame1` and `port2` and `frame2`, while
also avoiding clashing with other names (`source`, `target`) which have
other meaning in the `window.postMessage` context.

I have removed one level of nesting in the `PortProvider#discover` that
make the method more readable.

I added some other suggestions from PR #3881.
parent 3d8982f8
...@@ -7,8 +7,7 @@ const POLLING_INTERVAL_FOR_PORT = 250; ...@@ -7,8 +7,7 @@ const POLLING_INTERVAL_FOR_PORT = 250;
/** /**
* @typedef {import('../types/annotator').Destroyable} Destroyable * @typedef {import('../types/annotator').Destroyable} Destroyable
* @typedef {import('./port-util').Message} Message * @typedef {import('./port-util').Message} Message
* @typedef {Message['channel']} Channel * @typedef {import('./port-util').Frame} Frame
* @typedef {Message['port']} Port
*/ */
/** /**
...@@ -16,14 +15,18 @@ const POLLING_INTERVAL_FOR_PORT = 250; ...@@ -16,14 +15,18 @@ const POLLING_INTERVAL_FOR_PORT = 250;
* MessagePort-based connection to other frames. It is used together with * MessagePort-based connection to other frames. It is used together with
* PortProvider which runs in the host frame. See PortProvider for an overview. * PortProvider which runs in the host frame. See PortProvider for an overview.
* *
* 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 * @implements Destroyable
*/ */
export class PortFinder { export class PortFinder {
constructor() { /**
* @param {object} options
* @param {Exclude<Frame, 'host'>} options.source - the role of this frame
* @param {Window} options.hostFrame - the frame where the `PortProvider` is
* listening for messages.
*/
constructor({ hostFrame, source }) {
this._hostFrame = hostFrame;
this._source = source;
this._listeners = new ListenerCollection(); this._listeners = new ListenerCollection();
} }
...@@ -34,20 +37,16 @@ export class PortFinder { ...@@ -34,20 +37,16 @@ export class PortFinder {
/** /**
* Request a specific port from `hostFrame` * Request a specific port from `hostFrame`
* *
* @param {object} options * @param {Frame} target - the frame aiming to be discovered
* @param {Channel} options.channel - requested channel
* @param {Window} options.hostFrame - frame where the hypothesis client is
* loaded and `PortProvider` is listening for messages
* @param {Port} options.port - requested port
* @return {Promise<MessagePort>} * @return {Promise<MessagePort>}
*/ */
async discover({ channel, hostFrame, port }) { async discover(target) {
let isValidRequest = false; let isValidRequest = false;
if ( if (
(channel === 'guest-host' && port === 'guest') || (this._source === 'guest' && target === 'host') ||
(channel === 'guest-sidebar' && port === 'guest') || (this._source === 'guest' && target === 'sidebar') ||
(channel === 'host-sidebar' && port === 'sidebar') || (this._source === 'sidebar' && target === 'host') ||
(channel === 'notebook-sidebar' && port === 'notebook') (this._source === 'notebook' && target === 'sidebar')
) { ) {
isValidRequest = true; isValidRequest = true;
} }
...@@ -57,26 +56,34 @@ export class PortFinder { ...@@ -57,26 +56,34 @@ export class PortFinder {
} }
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
function postRequest() { const postRequest = () => {
hostFrame.postMessage( this._hostFrame.postMessage(
{ channel, port, source: 'hypothesis', type: 'request' }, {
authority: 'hypothesis',
frame1: this._source,
frame2: target,
type: 'request',
},
'*' '*'
); );
} };
// In some situations, because `guest` iframe/s load in parallel to the `host` // Because `guest` iframes load in parallel to the `host` frame, we can
// frame, we can not assume that the code in the `host` frame is executed before // not assume that the code in the `host` frame is executed before the
// the code in a `guest` frame. Hence, we can't assume that `PortProvider` (in // code in a `guest` frame. Hence, we can't assume that `PortProvider` (in
// the `host` frame) is initialized before `PortFinder` (in the non-host frames). // the `host` frame) is initialized before `PortFinder` (in the non-host
// Therefore, for the `PortFinder`, we implement a polling strategy (sending a // frames). Therefore, for the `PortFinder`, we implement a polling
// message every N milliseconds) until a response is received. // strategy (sending a message every N milliseconds) until a response is
// received.
const intervalId = setInterval(postRequest, POLLING_INTERVAL_FOR_PORT); const intervalId = setInterval(postRequest, POLLING_INTERVAL_FOR_PORT);
// The `host` frame maybe busy, that's why we should wait. // The `host` frame maybe busy, that's why we should wait.
const timeoutId = setTimeout(() => { const timeoutId = setTimeout(() => {
clearInterval(intervalId); clearInterval(intervalId);
reject( reject(
new Error(`Unable to find '${port}' port on '${channel}' channel`) new Error(
`Unable to establish ${this._source}-${target} communication channel`
)
); );
}, MAX_WAIT_FOR_PORT); }, MAX_WAIT_FOR_PORT);
...@@ -84,9 +91,9 @@ export class PortFinder { ...@@ -84,9 +91,9 @@ export class PortFinder {
const { data, ports } = /** @type {MessageEvent} */ (event); const { data, ports } = /** @type {MessageEvent} */ (event);
if ( if (
isMessageEqual(data, { isMessageEqual(data, {
channel, authority: 'hypothesis',
port, frame1: this._source,
source: 'hypothesis', frame2: target,
type: 'offer', type: 'offer',
}) })
) { ) {
......
This diff is collapsed.
...@@ -3,20 +3,19 @@ ...@@ -3,20 +3,19 @@
// 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 security feature but an // but different source. This is not a security feature but an
// anti-collision mechanism. // anti-collision mechanism.
const SOURCE = 'hypothesis'; const AUTHORITY = 'hypothesis';
/** /**
* These types are the used in by `PortProvider` and `PortFinder` for the * These types are the used in by `PortProvider` and `PortFinder` for the
* inter-frame discovery and communication processes. * inter-frame discovery and communication processes.
* *
* @typedef {'guest-host'|'guest-sidebar'|'host-sidebar'|'notebook-sidebar'} Channel * @typedef {'guest'|'host'|'notebook'|'sidebar'} Frame
* @typedef {'guest'|'host'|'notebook'|'sidebar'} Port
* *
* @typedef Message * @typedef Message
* @prop {Channel} channel * @prop {AUTHORITY} authority
* @prop {Port} port * @prop {Frame} frame1
* @prop {'offer'|'request'} type * @prop {Frame} frame2
* @prop {SOURCE} source - * @prop {'offer'|'request'} type
*/ */
/** /**
...@@ -26,18 +25,18 @@ const SOURCE = 'hypothesis'; ...@@ -26,18 +25,18 @@ const SOURCE = 'hypothesis';
* @param {any} data * @param {any} data
* @return {data is Message} * @return {data is Message}
*/ */
function isMessageValid(data) { function isMessage(data) {
if (data === null || typeof data !== 'object') { if (data === null || typeof data !== 'object') {
return false; return false;
} }
for (let property of ['channel', 'port', 'source', 'type']) { for (let property of ['frame1', 'frame2', 'type']) {
if (typeof data[property] !== 'string') { if (typeof data[property] !== 'string') {
return false; return false;
} }
} }
return data.source === SOURCE; return data.authority === AUTHORITY;
} }
/** /**
...@@ -47,7 +46,7 @@ function isMessageValid(data) { ...@@ -47,7 +46,7 @@ function isMessageValid(data) {
* @param {Message} message * @param {Message} message
*/ */
export function isMessageEqual(data, message) { export function isMessageEqual(data, message) {
if (!isMessageValid(data)) { if (!isMessage(data)) {
return false; return false;
} }
......
...@@ -4,9 +4,19 @@ import { PortFinder } from '../port-finder'; ...@@ -4,9 +4,19 @@ import { PortFinder } from '../port-finder';
const MAX_WAIT_FOR_PORT = 1000 * 5; const MAX_WAIT_FOR_PORT = 1000 * 5;
describe('PortFinder', () => { describe('PortFinder', () => {
const authority = 'hypothesis';
const frame1 = 'guest';
const type = 'offer';
let portFinder; let portFinder;
let portFinders;
function portProviderOffer({ data, ports = [] }) { function createPortFinder(source = frame1) {
const instance = new PortFinder({ hostFrame: window, source });
portFinders.push(instance);
return instance;
}
function sendPortProviderOffer({ data, ports = [] }) {
const event = new MessageEvent('message', { const event = new MessageEvent('message', {
data, data,
ports, ports,
...@@ -18,34 +28,35 @@ describe('PortFinder', () => { ...@@ -18,34 +28,35 @@ describe('PortFinder', () => {
} }
beforeEach(() => { beforeEach(() => {
portFinders = [];
sinon.stub(window, 'postMessage'); sinon.stub(window, 'postMessage');
portFinder = new PortFinder(); portFinder = createPortFinder();
}); });
afterEach(() => { afterEach(() => {
window.postMessage.restore(); window.postMessage.restore();
portFinder.destroy(); portFinders.forEach(instance => instance.destroy());
}); });
describe('#destroy', () => { describe('#destroy', () => {
it('ignores subsequent `offer` messages of ports', async () => { it('ignores subsequent `offer` messages of ports', async () => {
let error; let error;
const channel = 'host-sidebar'; const target = 'host';
const port = 'sidebar';
const { port1 } = new MessageChannel(); const { port1 } = new MessageChannel();
const clock = sinon.useFakeTimers(); const clock = sinon.useFakeTimers();
try { try {
portFinder portFinder.discover(target).catch(e => (error = e));
.discover({
channel,
hostFrame: window,
port,
})
.catch(e => (error = e));
portFinder.destroy(); portFinder.destroy();
portProviderOffer({
data: { channel, port, source: 'hypothesis', type: 'offer' }, sendPortProviderOffer({
data: {
authority,
frame1,
frame2: target,
type,
},
ports: [port1], ports: [port1],
}); });
clock.tick(MAX_WAIT_FOR_PORT); clock.tick(MAX_WAIT_FOR_PORT);
...@@ -57,25 +68,17 @@ describe('PortFinder', () => { ...@@ -57,25 +68,17 @@ describe('PortFinder', () => {
assert.equal( assert.equal(
error.message, error.message,
"Unable to find 'sidebar' port on 'host-sidebar' channel" 'Unable to establish guest-host communication channel'
); );
}); });
}); });
describe('#discover', () => { describe('#discover', () => {
[ ['guest', 'invalid'].forEach(target =>
{ channel: 'invalid', port: 'guest' },
{ channel: 'guest-host', port: 'invalid' },
{ channel: 'guest-host', port: 'host' },
].forEach(({ channel, port }) =>
it('rejects if requesting an invalid port', async () => { it('rejects if requesting an invalid port', async () => {
let error; let error;
try { try {
await portFinder.discover({ await portFinder.discover(target);
channel,
hostFrame: window,
port,
});
} catch (e) { } catch (e) {
error = e; error = e;
} }
...@@ -84,24 +87,24 @@ describe('PortFinder', () => { ...@@ -84,24 +87,24 @@ describe('PortFinder', () => {
); );
[ [
{ channel: 'guest-host', port: 'guest' }, { source: 'guest', target: 'host' },
{ channel: 'guest-sidebar', port: 'guest' }, { source: 'guest', target: 'sidebar' },
{ channel: 'host-sidebar', port: 'sidebar' }, { source: 'sidebar', target: 'host' },
{ channel: 'notebook-sidebar', port: 'notebook' }, { source: 'notebook', target: 'sidebar' },
].forEach(({ channel, port }) => ].forEach(({ source, target }) =>
it('resolves if requesting a valid port', async () => { it('resolves if requesting a valid port', async () => {
const { port1 } = new MessageChannel(); const { port1 } = new MessageChannel();
let resolvedPort; let resolvedPort;
portFinder = createPortFinder(source);
portFinder
.discover({ portFinder.discover(target).then(port => (resolvedPort = port));
channel, sendPortProviderOffer({
hostFrame: window, data: {
port, authority,
}) frame1: source,
.then(port => (resolvedPort = port)); frame2: target,
portProviderOffer({ type,
data: { channel, port, source: 'hypothesis', type: 'offer' }, },
ports: [port1], ports: [port1],
}); });
await delay(0); await delay(0);
...@@ -112,18 +115,11 @@ describe('PortFinder', () => { ...@@ -112,18 +115,11 @@ describe('PortFinder', () => {
it("times out if host doesn't respond", async () => { it("times out if host doesn't respond", async () => {
let error; let error;
const channel = 'host-sidebar'; const target = 'host';
const port = 'sidebar';
const clock = sinon.useFakeTimers(); const clock = sinon.useFakeTimers();
try { try {
portFinder portFinder.discover(target).catch(e => (error = e));
.discover({
channel,
hostFrame: window,
port,
})
.catch(e => (error = e));
clock.tick(MAX_WAIT_FOR_PORT); clock.tick(MAX_WAIT_FOR_PORT);
} finally { } finally {
clock.restore(); clock.restore();
...@@ -132,7 +128,7 @@ describe('PortFinder', () => { ...@@ -132,7 +128,7 @@ describe('PortFinder', () => {
assert.callCount(window.postMessage, 21); assert.callCount(window.postMessage, 21);
assert.alwaysCalledWithExactly( assert.alwaysCalledWithExactly(
window.postMessage, window.postMessage,
{ channel, port, source: 'hypothesis', type: 'request' }, { authority, frame1, frame2: target, type: 'request' },
'*' '*'
); );
...@@ -140,7 +136,7 @@ describe('PortFinder', () => { ...@@ -140,7 +136,7 @@ describe('PortFinder', () => {
assert.equal( assert.equal(
error.message, error.message,
"Unable to find 'sidebar' port on 'host-sidebar' channel" 'Unable to establish guest-host communication channel'
); );
}); });
}); });
......
import { delay } from '../../test-util/wait'; import { delay } from '../../test-util/wait';
import { PortProvider } from '../port-provider'; import { PortProvider } from '../port-provider';
const source = 'hypothesis'; const authority = 'hypothesis';
describe('PortProvider', () => { describe('PortProvider', () => {
let portProvider; let portProvider;
function portFinderRequest({ async function sendPortFinderRequest({
data, data,
origin = window.location.origin, origin = window.location.origin,
source = window, source = window,
...@@ -18,6 +18,7 @@ describe('PortProvider', () => { ...@@ -18,6 +18,7 @@ describe('PortProvider', () => {
}); });
window.dispatchEvent(event); window.dispatchEvent(event);
await delay(0);
return event; return event;
} }
...@@ -25,7 +26,6 @@ describe('PortProvider', () => { ...@@ -25,7 +26,6 @@ describe('PortProvider', () => {
beforeEach(() => { beforeEach(() => {
sinon.stub(window, 'postMessage'); sinon.stub(window, 'postMessage');
portProvider = new PortProvider(window.location.origin); portProvider = new PortProvider(window.location.origin);
portProvider.listen();
}); });
afterEach(() => { afterEach(() => {
...@@ -36,97 +36,76 @@ describe('PortProvider', () => { ...@@ -36,97 +36,76 @@ describe('PortProvider', () => {
describe('#destroy', () => { describe('#destroy', () => {
it('ignores valid port request if `PortFinder` has been destroyed', async () => { it('ignores valid port request if `PortFinder` has been destroyed', async () => {
portProvider.destroy(); portProvider.destroy();
portFinderRequest({ await sendPortFinderRequest({
data: { data: {
channel: 'host-sidebar', authority,
port: 'sidebar', frame1: 'sidebar',
source, frame2: 'host',
type: 'request', type: 'request',
}, },
}); });
await delay(0);
assert.notCalled(window.postMessage); assert.notCalled(window.postMessage);
}); });
}); });
describe('#getPort', () => { describe('#sidebarPort', () => {
it('returns `null` if called with wrong arguments', () => { it('returns the `host` port of the `sidebar-host` channel', () => {
let hostPort; assert.instanceOf(portProvider.sidebarPort, MessagePort);
// Incorrect channel
hostPort = portProvider.getPort({
channel: 'notebook-sidebar',
port: 'host',
});
assert.isNull(hostPort);
// Incorrect port
hostPort = portProvider.getPort({
channel: 'host-sidebar',
port: 'sidebar',
});
assert.isNull(hostPort);
});
it('returns the `host` port of the `host-sidebar` channel if called with the right arguments', () => {
const hostPort = portProvider.getPort({
channel: 'host-sidebar',
port: 'host',
});
assert.exists(hostPort);
}); });
}); });
describe('#listen', () => { describe('#listen', () => {
it('ignores all port requests before `listen` is called', async () => { it('ignores all port requests before `listen` is called', async () => {
portProvider.listen();
portProvider.destroy(); portProvider.destroy();
portProvider = new PortProvider(window.location.origin); portProvider = new PortProvider(window.location.origin);
const data = { const data = {
channel: 'host-sidebar', authority,
port: 'sidebar', frame1: 'sidebar',
source, frame2: 'host',
type: 'request', type: 'request',
}; };
portFinderRequest({ await sendPortFinderRequest({
data, data,
}); });
await delay(0);
assert.notCalled(window.postMessage); assert.notCalled(window.postMessage);
portProvider.listen(); portProvider.listen();
portFinderRequest({ await sendPortFinderRequest({
data, data,
}); });
await delay(0);
assert.calledOnce(window.postMessage); assert.calledOnce(window.postMessage);
}); });
}); });
describe('listens for port requests', () => { describe('listens for port requests', () => {
it('ignores port requests with invalid sources', async () => { [
const data = { { source: null, reason: 'source is null' },
channel: 'host-sidebar', {
port: 'sidebar',
source,
type: 'request',
};
portFinderRequest({
data,
source: null,
});
portFinderRequest({
data,
source: new MessageChannel().port1, source: new MessageChannel().port1,
}); reason: 'source is a MessageChannel',
await delay(0); },
].forEach(({ source, reason }) =>
it(`ignores port requests if ${reason}`, async () => {
portProvider.listen();
const data = {
authority,
frame1: 'sidebar',
frame2: 'host',
type: 'request',
};
assert.notCalled(window.postMessage); await sendPortFinderRequest({
}); data,
source,
});
assert.notCalled(window.postMessage);
})
);
[ [
// Disabled this check because it make axes-core to crash // Disabled this check because it make axes-core to crash
...@@ -134,71 +113,73 @@ describe('PortProvider', () => { ...@@ -134,71 +113,73 @@ describe('PortProvider', () => {
//{ data: null, reason: 'if message is null' }, //{ data: null, reason: 'if message is null' },
{ {
data: { data: {
channel: 'sidebar-host', // invalid channel (swapped words) authority: 'dummy', // invalid authority
port: 'sidebar', frame1: 'sidebar',
source, frame2: 'host',
type: 'request', type: 'request',
}, },
reason: 'if message contains an invalid channel', reason: 'contains an invalid authority',
}, },
{ {
data: { data: {
channel: 'host-sidebar', authority,
port: 'host', // invalid port frame1: 'host', // invalid source
source, frame2: 'host',
type: 'request', type: 'request',
}, },
reason: 'if message contains an invalid port', reason: 'contains an invalid frame1',
}, },
{ {
data: { data: {
channel: 'host-sidebar', authority,
port: 'sidebar', frame1: 'sidebar',
source: 'dummy', frame2: 'dummy', // invalid target
type: 'request', type: 'request',
}, },
reason: 'if message contains an invalid source', reason: 'contains an invalid frame2',
}, },
{ {
data: { data: {
channel: 'host-sidebar', authority,
port: 'dummy', frame1: 'sidebar',
source, frame2: 'host',
type: 'offer', // invalid offer type: 'offer', // invalid type
}, },
reason: 'if message contains an invalid offer', reason: 'contains an invalid type',
}, },
{ {
data: { data: {
channel: 'host-sidebar', authority,
port: 'sidebar', frame1: 'sidebar',
source, frame2: 'host',
type: 'request', type: 'request',
}, },
origin: 'https://dummy.com', origin: 'https://dummy.com',
reason: 'if message comes from invalid origin', reason: 'comes from invalid origin',
}, },
].forEach(({ data, reason, origin }) => { ].forEach(({ data, reason, origin }) => {
it(`ignores port request ${reason}`, async () => { it(`ignores port request if message ${reason}`, async () => {
portFinderRequest({ data, origin: origin ?? window.location.origin }); portProvider.listen();
await sendPortFinderRequest({
await delay(0); data,
origin: origin ?? window.location.origin,
});
assert.notCalled(window.postMessage); assert.notCalled(window.postMessage);
}); });
}); });
it('responds to a valid port request', async () => { it('responds to a valid port request', async () => {
portProvider.listen();
const data = { const data = {
channel: 'host-sidebar', authority,
port: 'sidebar', frame1: 'sidebar',
source, frame2: 'host',
type: 'request', type: 'request',
}; };
portFinderRequest({ await sendPortFinderRequest({
data, data,
}); });
await delay(0);
assert.calledWith( assert.calledWith(
window.postMessage, window.postMessage,
...@@ -209,19 +190,19 @@ describe('PortProvider', () => { ...@@ -209,19 +190,19 @@ describe('PortProvider', () => {
}); });
it('responds to the first valid port request but ignores additional requests', async () => { it('responds to the first valid port request but ignores additional requests', async () => {
portProvider.listen();
const data = { const data = {
channel: 'guest-host', authority,
port: 'guest', frame1: 'guest',
source, frame2: 'sidebar',
type: 'request', type: 'request',
}; };
for (let i = 0; i < 4; ++i) { for (let i = 0; i < 4; ++i) {
portFinderRequest({ await sendPortFinderRequest({
data, data,
}); });
} }
await delay(0);
assert.calledOnceWithExactly( assert.calledOnceWithExactly(
window.postMessage, window.postMessage,
...@@ -232,30 +213,29 @@ describe('PortProvider', () => { ...@@ -232,30 +213,29 @@ describe('PortProvider', () => {
}); });
it('sends the counterpart port via the sidebar port', async () => { it('sends the counterpart port via the sidebar port', async () => {
portFinderRequest({ portProvider.listen();
await sendPortFinderRequest({
data: { data: {
channel: 'host-sidebar', authority,
port: 'sidebar', frame1: 'sidebar',
source, frame2: 'host',
type: 'request', type: 'request',
}, },
}); });
await delay(0);
const [sidebarPort] = window.postMessage.getCall(0).args[2]; const [sidebarPort] = window.postMessage.getCall(0).args[2];
const handler = sinon.stub(); const handler = sinon.stub();
sidebarPort.onmessage = handler; sidebarPort.onmessage = handler;
const data = { const data = {
channel: 'guest-sidebar', authority,
port: 'guest', frame1: 'guest',
source, frame2: 'sidebar',
type: 'request', type: 'request',
}; };
portFinderRequest({ await sendPortFinderRequest({
data, data,
}); });
await delay(0);
assert.calledWith( assert.calledWith(
handler, handler,
...@@ -266,18 +246,18 @@ describe('PortProvider', () => { ...@@ -266,18 +246,18 @@ describe('PortProvider', () => {
}); });
it('sends the counterpart port via the listener', async () => { it('sends the counterpart port via the listener', async () => {
portProvider.listen();
const handler = sinon.stub(); const handler = sinon.stub();
portProvider.on('hostPortRequest', handler); portProvider.on('hostPortRequest', handler);
const data = { const data = {
channel: 'guest-host', authority,
port: 'guest', frame1: 'guest',
source, frame2: 'host',
type: 'request', type: 'request',
}; };
portFinderRequest({ await sendPortFinderRequest({
data, data,
}); });
await delay(0);
assert.calledWith(handler, 'guest', sinon.match.instanceOf(MessagePort)); assert.calledWith(handler, 'guest', sinon.match.instanceOf(MessagePort));
}); });
......
import { isMessageEqual } from '../port-util'; import { isMessageEqual } from '../port-util';
const source = 'hypothesis';
describe('port-util', () => { describe('port-util', () => {
describe('isMessageEqual', () => { describe('isMessageEqual', () => {
const authority = 'hypothesis';
const frame1 = 'guest';
const frame2 = 'sidebar';
const type = 'offer';
[ [
{ {
data: { data: {
channel: 'host-sidebar', authority,
port: 'guest', frame1,
source, frame2,
type: 'offer', type,
},
message: {
channel: 'host-sidebar',
port: 'guest',
source,
type: 'offer',
}, },
expectedResult: true, expectedResult: true,
reason: 'data matches the message', reason: 'data matches the message',
}, },
{ {
data: { data: {
channel: 'host-sidebar', authority,
port: 'guest', frame1,
source, frame2,
type: 'offer', type,
},
message: {
source,
type: 'offer',
channel: 'host-sidebar',
port: 'guest',
}, },
expectedResult: true, expectedResult: true,
reason: 'data matches the message (properties in different order)', reason: 'data matches the message (properties in different order)',
}, },
{ {
data: null, data: null,
message: {
channel: 'host-sidebar',
port: 'guest',
source,
type: 'offer',
},
expectedResult: false, expectedResult: false,
reason: 'data is null', reason: 'data is null',
}, },
{ {
data: { data: {
channel: 'host-sidebar', authority,
port: 9, // wrong type // frame1 property missing
source, frame2,
type: 'offer', type,
},
message: {
channel: 'host-sidebar',
port: 'guest',
source,
type: 'offer',
}, },
expectedResult: false, expectedResult: false,
reason: 'data has a property with the wrong type', reason: 'data has one property that is missing',
}, },
{ {
data: { data: {
// channel property missing authority,
port: 'guest', frame1,
source, frame2: 9, // wrong type
type: 'offer', type,
},
message: {
channel: 'host-sidebar',
port: 'guest',
source,
type: 'offer',
}, },
expectedResult: false, expectedResult: false,
reason: 'data has one property that is missing', reason: 'data has one property with a wrong type',
}, },
{ {
data: { data: {
channel: 'host-sidebar', authority,
port: 'guest',
source,
type: 'offer',
extra: 'dummy', // additional extra: 'dummy', // additional
}, frame1,
message: { frame2,
channel: 'host-sidebar', type,
port: 'guest',
source,
type: 'offer',
}, },
expectedResult: false, expectedResult: false,
reason: 'data has one additional property', reason: 'data has one additional property',
}, },
{ {
data: { data: {
channel: 'host-sidebar', authority,
port: 'guest', frame1: 'dummy', // different
source, frame2,
type: 'offer', type,
window, // not serializable
},
message: {
channel: 'host-sidebar',
port: 'guest',
source,
type: 'offer',
}, },
expectedResult: false, expectedResult: false,
reason: "data has one property that can't be serialized", reason: 'data has one property that is different',
}, },
{ {
data: { data: {
channel: 'host-sidebar', authority,
port: 'guest', frame1,
source, frame2,
type: 'offer', type,
}, window, // not serializable
message: {
channel: 'guest-sidebar', // different
port: 'guest',
source,
type: 'offer',
}, },
expectedResult: false, expectedResult: false,
reason: 'data has one property that is different', reason: "data has one property that can't be serialized",
}, },
].forEach(({ data, message, expectedResult, reason }) => { ].forEach(({ data, expectedResult, reason }) => {
it(`returns '${expectedResult}' because the ${reason}`, () => { it(`returns '${expectedResult}' because the ${reason}`, () => {
const result = isMessageEqual(data, message); const result = isMessageEqual(data, {
authority,
frame1,
frame2,
type,
});
assert.equal(result, expectedResult); assert.equal(result, expectedResult);
}); });
}); });
......
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