pebble/sdk/tests/include/test_message.js
2025-01-27 11:38:16 -08:00

457 lines
16 KiB
JavaScript

/**
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-env mocha */
/* eslint func-names: 0 */
const assert = require('assert');
const unroll = require('unroll');
unroll.use(it);
// Override setTimeout() to fire immediately:
var origSetTimeout = setTimeout;
setTimeout = function(f, t) {
origSetTimeout(f.bind(undefined), 0);
};
describe('Pebble', () => {
var mockPebble;
const simulateReceivingAppMessageEvent = (payload) => {
const appMessageEvent = {
name: 'appmessage',
payload: payload
};
global.Pebble.handleEvent(appMessageEvent);
};
const enterSessionOpen = () => {
global.Pebble.handleEvent({ name : "ready" });
var data = new Uint8Array(6);
data[0] = 1;
data[1] = 3;
data[2] = 0;
data[3] = 155;
data[4] = 0;
data[5] = 155;
simulateReceivingAppMessageEvent({ 'ControlKeyResetComplete' : Array.from(data) });
mockPebble.sendAppMessage.reset();
};
const createChunk = (offset, size, data) => {
if (offset == 0) { // First msg
var isFirst = (1 << 7);
var n = size + 1;
} else {
var isFirst = 0;
var n = offset;
}
var rv = [ (n) & 255,
(n >> 8) & 255,
(n >> 16) & 255,
((n >> 24) & ~(1 << 7)) | isFirst ];
Array.prototype.push.apply(rv, data.slice(offset, offset + size));
if (offset + size == data.length) {
rv.push(0);
}
return { "ControlKeyChunk" : rv };
};
const simulateReceivingPostMessageChunk = () => {
var data = '{ "msg_num" : 0 }'.split('').map(function(x) { return x.charCodeAt(0); });
var chunk = createChunk(0, data.length, data);
simulateReceivingAppMessageEvent(chunk);
};
beforeEach(() => {
// Create a new mock for the Pebble global object for each test:
const PebbleMockConstructor = require('./pebble-mock.js');
global.Pebble = new PebbleMockConstructor();
// Keep a reference to the mock that will be "wrapped" as soon as _pkjs_message_wrapper.js
// is loaded...
mockPebble = global.Pebble;
// Reload it to 'patch' the Pebble object:
const message_js_path = '../../include/_pkjs_message_wrapper.js';
delete require.cache[require.resolve(message_js_path)];
require(message_js_path);
enterSessionOpen();
});
/****************************************************************************
* Message Encoding
***************************************************************************/
describe('interprets received postMessage API data as UTF-8', () => {
unroll('interprets #utf8_data as #result', (done, fixture) => {
global.Pebble.on('message', (event) => {
assert.equal(event.type, 'message');
assert.equal(event.data, fixture.result);
done();
});
const payload = createChunk(0, fixture.utf8_data.length, fixture.utf8_data);
if (fixture.result instanceof Error) {
assert.throws(() => {
simulateReceivingAppMessageEvent(payload);
}, typeof(fixture.result), fixture.result.message);
done();
} else {
simulateReceivingAppMessageEvent(payload);
}
}, [
['utf8_data', 'result'],
// empty string:
[[34, 34], ''],
// Pile of Poo, in double quotes:
[[34, 240, 159, 146, 169,34], '💩'],
// Surrogates are illegal in UTF-8:
[[34, 0xED, 0xA0, 0xB5, 0xED, 0xBC, 0x80, 34], Error('Lone surrogate U+D835 is not a scalar value')],
// 2-byte code point, in double quotes:
[[34, 196, 145, 34], '\u0111'],
// 3-byte codepoint, in double quotes:
[[34, 0xE0, 0xA0, 0x95, 34], '\u0815']
]);
});
describe('encodes sent postMessage API data as UTF-8', () => {
unroll('encodes #input as #utf8_data', (done, fixture) => {
global.Pebble.postMessage(fixture.input);
assert.equal(mockPebble.sendAppMessage.callCount, 1);
const lastAppMessage = mockPebble.sendAppMessage.lastCall.args[0];
assert.deepEqual(lastAppMessage['ControlKeyChunk'].slice(4), fixture.utf8_data);
done();
}, [
['input', 'utf8_data'],
// empty string:
['', [34, 34, 0]],
// Pile of Poo, in double quotes:
['💩', [34, 240, 159, 146, 169, 34, 0]],
// 2-byte code point, in double quotes:
['\u0111', [34, 196, 145, 34, 0]],
// 3-byte codepoint, in double quotes:
['\u0815', [34, 0xE0, 0xA0, 0x95, 34, 0]]
]);
});
/****************************************************************************
* Message Handlers
***************************************************************************/
describe('Ensure that AppMessage is blocked', () => {
it('tries to register a Pebble.on("appmessage") handler', (done) => {
assert.throws(() => {
global.Pebble.on('appmessage', (e) => {
assert(0, "Should not have been called");
});
}, /not supported/);
// If this results in our callback being called, we'll throw an Error().
simulateReceivingAppMessageEvent({ 'KEY' : 'DATA' });
done();
});
it('tries to Pebble.addEventListener("appmessage")', (done) => {
assert.throws(() => {
global.Pebble.addEventListener('appmessage', (e) => {
// This will be thrown if the eventlistener was registered
assert(0, "Should not have been called");
});
}, /not supported/);
// If this results in our callback being called, we'll throw an Error().
simulateReceivingAppMessageEvent({ 'KEY' : 'DATA' });
done();
});
it('tries to call Pebble.sendAppMessage()', (done) => {
assert.notStrictEqual(typeof global.Pebble.sendAppMessage, 'function');
assert.equal(global.Pebble.sendAppMessage, undefined);
done();
});
});
describe('registers multiple message handlers', () => {
unroll('registers #num_handlers handlers to receive #num_messages messages each', (done, fixture) => {
var callback_count = 0;
var handler = function(e) { ++callback_count; };
for (var h = 0; h < fixture.num_handlers; ++h) {
global.Pebble.on('message', handler);
}
for (var i = 0; i < fixture.num_messages; ++i) {
simulateReceivingPostMessageChunk();
}
assert.equal(callback_count, fixture.num_handlers * fixture.num_messages);
done();
}, [
[ 'num_handlers', 'num_messages' ],
[ 1, 1 ],
[ 2, 1 ],
[ 3, 2 ],
]);
});
describe('registers multiple message handlers, unsubscribes one', () => {
unroll('registers #num_handlers, then unregisters #num_unregister', (done, fixture) => {
var callback_count = 0;
var handler = function(e) { ++callback_count; };
for (var h = 0; h < fixture.num_handlers; ++h) {
global.Pebble.on('message', handler);
}
for (var u = 0; u < fixture.num_unregister; ++u) {
global.Pebble.off('message', handler);
}
simulateReceivingPostMessageChunk();
assert.equal(callback_count, fixture.num_handlers - fixture.num_unregister);
done();
}, [
[ 'num_handlers', 'num_unregister' ],
[ 4, 2 ],
[ 10, 10 ],
]);
});
describe('call Pebble.off("message", handler) from within that event handler', () => {
unroll('calling while #num_registered other handlers are registered', (done, fixture) => {
var callback_count = 0;
var handler = function(e) { ++callback_count; };
var remove_handler = function(e) { ++callback_count; global.Pebble.off('message', remove_handler); }
global.Pebble.on('message', remove_handler);
for (var i = 0; i < fixture.num_registered; ++i) {
global.Pebble.on('message', handler);
}
simulateReceivingPostMessageChunk();
assert.equal(callback_count, fixture.num_registered + 1);
// Now that the remove_handler has been removed, send another and make
// sure that we have one less called.
callback_count = 0;
simulateReceivingPostMessageChunk();
assert.equal(callback_count, fixture.num_registered);
done();
}, [
[ 'num_registered' ],
[ 0 ],
[ 1 ],
[ 10 ],
]);
});
/****************************************************************************
* postmessageerror event
***************************************************************************/
describe('postmessageerror Event', () => {
it('event.data is set to the object that was attempted to be sent', (done) => {
global.Pebble.handleEvent({ name : "ready" });
mockPebble.sendAppMessage.reset();
global.Pebble.on('postmessageerror', function(e) {
assert.deepEqual(e.data, {b: 'c'});
done();
});
var a = { b: 'c' };
global.Pebble.postMessage(a);
a.b = 'd'; // modify to test that a copy of 'a' is sent
});
});
/****************************************************************************
* postmessageconnected / postmessagedisconnected event
***************************************************************************/
describe('Connection Events', () => {
unroll('postmessageconnected. Start connected: #start_connected', (done, fixture) => {
var connected_call_count = 0;
if (!fixture.start_connected) {
// Disconnect
global.Pebble.handleEvent({ name : "ready" });
}
global.Pebble.on('postmessageconnected', function(e) {
assert.equal(e.type, 'postmessageconnected');
++connected_call_count;
});
enterSessionOpen(); // establish connection
if (fixture.start_connected) {
assert.equal(connected_call_count, 2);
} else {
assert.equal(connected_call_count, 1);
}
done();
}, [
[ 'start_connected' ],
[ true, ],
[ false, ],
]);
unroll('postmessagedisconnected. Start disconnected: #start_disconnected', (done, fixture) => {
var disconnected_call_count = 0;
if (fixture.start_disconnected) {
// Disconnect
global.Pebble.handleEvent({ name : "ready" });
}
global.Pebble.on('postmessagedisconnected', function(e) {
assert.equal(e.type, 'postmessagedisconnected');
++disconnected_call_count;
});
if (fixture.start_disconnected) {
// Need to establish a connection before we can disconnect
enterSessionOpen();
}
global.Pebble.handleEvent({ name : "ready" }); // Disconnect again
if (fixture.start_disconnected) {
assert.equal(disconnected_call_count, 2);
} else {
assert.equal(disconnected_call_count, 1);
}
done();
}, [
[ 'start_disconnected' ],
[ true, ],
[ false, ],
]);
});
/****************************************************************************
* Control Layer
***************************************************************************/
describe('Control Layer', () => {
it('Ready message => ResetRequest', (done) => {
global.Pebble.handleEvent({ name : "ready" });
assert.equal(mockPebble.sendAppMessage.callCount, 1);
assert('ControlKeyResetRequest' in mockPebble.sendAppMessage.lastCall.args[0]);
done();
});
it ('Disconnected => AwaitingResetCompleteLocalInitiated => SessionOpen', (done) => {
global.Pebble.handleEvent({ name : "ready" });
mockPebble.sendAppMessage.reset();
var data = new Uint8Array(6);
data[0] = 1;
data[1] = 3;
data[2] = 0;
data[3] = 155;
data[4] = 0;
data[5] = 155;
simulateReceivingAppMessageEvent({ 'ControlKeyResetComplete' : Array.from(data) });
assert.equal(mockPebble.sendAppMessage.callCount, 1);
assert('ControlKeyResetComplete' in mockPebble.sendAppMessage.lastCall.args[0]);
done();
});
it ('Disconnected => AwaitingResetCompleteLocalInitiated => UnsupportedError', (done) => {
global.Pebble.handleEvent({ name : "ready" });
mockPebble.sendAppMessage.reset();
var data = new Uint8Array(6);
data[0] = 155; // Unsupported min version
data[1] = 156; // Unsupported max version
data[2] = 0;
data[3] = 155;
data[4] = 0;
data[5] = 155;
simulateReceivingAppMessageEvent({ 'ControlKeyResetComplete' : Array.from(data) });
assert.equal(mockPebble.sendAppMessage.callCount, 1);
assert('ControlKeyUnsupportedError' in mockPebble.sendAppMessage.lastCall.args[0]);
done();
});
it ('SessionOpen => AwaitingResetCompleteRemoteInitiated => UnsupportedError => Error', (done) => {
simulateReceivingAppMessageEvent({ 'ControlKeyResetRequest' : 0 });
assert.equal(mockPebble.sendAppMessage.callCount, 1);
assert('ControlKeyResetComplete' in mockPebble.sendAppMessage.lastCall.args[0]);
try {
simulateReceivingAppMessageEvent({ 'ControlKeyUnsupportedError' : "Test Error" });
} catch (e) {
assert.equal("Error: Unsupported protocol error: Test Error", e.toString());
}
done();
});
it ('Retry sending control message, check max retries.', (done) => {
// override setTimeout
setTimeout = function(fn, delay) {
fn(); // Use a synchronous call here because we want to make sure that there
// is a maximum of 3 callbacks. If we do these asynchronously,
// there is no nice way to test this.
}
// Replace our sendAppMessage with one that will always call the error callback
_mockSendAppMessage = mockPebble.sendAppMessage;
mockPebble.sendAppMessage = function(msg, complCb, errCb) {
_mockSendAppMessage(msg, undefined, errCb);
errCb(msg);
};
simulateReceivingAppMessageEvent({ 'ControlKeyResetRequest' : 0 });
// Should be called 1 + 3 retries, no more.
assert.equal(_mockSendAppMessage.callCount, 4);
done();
});
it('Retry sending control message, asynch', (done) => {
// This test will fail due to timeout if retry isn't working correctly.
var _setTimeout = setTimeout;
setTimeout = function(fn, delay) {
_setTimeout(fn, 0);
}
_mockSendAppMessage = mockPebble.sendAppMessage;
mockPebble.sendAppMessage = function(msg, complCb, errCb) {
_mockSendAppMessage(msg, undefined, errCb);
if (_mockSendAppMessage.callCount == 4) {
// 4 calls is 1 + 3 retries. We're done here
done();
} else {
_setTimeout(errCb.bind(msg), 0);
}
};
simulateReceivingAppMessageEvent({ 'ControlKeyResetRequest' : 0 });
});
});
it('.postMessage(nonJSONable) should throw a TypeError', (done) => {
var expectedMsg =
"Argument at index 0 is not a JSON.stringify()-able object";
assert.throws(
() => { global.Pebble.postMessage(undefined); }, TypeError, expectedMsg);
assert.throws(
() => { global.Pebble.postMessage(() => {}); }, TypeError, expectedMsg);
done()
});
});