Import of the watch repository from Pebble

This commit is contained in:
Matthieu Jeanson 2024-12-12 16:43:03 -08:00 committed by Katharine Berry
commit 3b92768480
10334 changed files with 2564465 additions and 0 deletions

View file

@ -0,0 +1,42 @@
/**
* 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.
*/
module.exports = {
"env": {
"browser": true,
"commonjs": true,
"es6": true
},
"extends": "eslint:recommended",
"rules": {
"indent": [
"error",
2
],
"linebreak-style": [
"error",
"unix"
],
"quotes": [
"error",
"single"
],
"semi": [
"error",
"always"
]
}
};

1
sdk/tests/include/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
node_modules

View file

@ -0,0 +1,50 @@
# sdk/include/*.js unit testing how-to
This folder contains tests for the .js code in sdk/include.
## Installing dependencies
1. `cd sdk/tests/include`
2. `npm install`
## Running tests
1. `cd sdk/tests/include`
2. `npm test` this runs the tests using the [mocha](http://mochajs.org/) test runner.
You should see some output, similar to this:
```
$ npm test
> pebble-pkjs-tests@1.0.0 test /Users/martijn/tintin/sdk/tests/include
> NODE_PATH=../include ./node_modules/mocha/bin/mocha *.js
Pebble
interprets received postMessage API data as UTF-8
✓ interprets [34,34] as ""
✓ interprets [34,240,159,146,169,34] as "💩"
✓ interprets [34,237,160,181,237,188,128,34] as {}
✓ interprets [34,196,145,34] as "đ"
✓ interprets [34,224,160,149,34] as "ࠕ"
encodes sent postMessage API data as UTF-8
sendAppMessage: [object Object]
✓ encodes "" as [34,34,0]
sendAppMessage: [object Object]
✓ encodes "💩" as [34,240,159,146,169,34,0]
sendAppMessage: [object Object]
✓ encodes "đ" as [34,196,145,34,0]
sendAppMessage: [object Object]
✓ encodes "ࠕ" as [34,224,160,149,34,0]
9 passing (25ms)
```
## Linting the test code
1. `cd sdk/tests/include`
2. `npm run-script lint`
## Adding tests
* You can add `test_xyz.js` files in the `tests` folder. It will automatically get picked up by the test runner.
* If you need to a mock for the global `Pebble` object, check out `pebble-mock.js`. It's probably worth using and extending that than to re-invent the wheel.
* When adding additional dependencies (node packages), make sure to install them using `npm install --save-dev <PACKAGE_NAME>` so that they get added to the `devDependencies` in the `package.json` file.

View file

@ -0,0 +1,23 @@
{
"name": "pebble-pkjs-tests",
"version": "1.0.0",
"description": "Unit tests for .js pkjs assets",
"main": "index.js",
"dependencies": {
"utf8": "^2.1.1"
},
"devDependencies": {
"eslint": "^2.10.2",
"eslint-plugin-import": "^1.11.1",
"eslint-plugin-jsx-a11y": "^2.0.1",
"mocha": "^2.5.3",
"simple-mock": "^0.7.0",
"unroll": "^1.1.0"
},
"scripts": {
"test": "NODE_PATH=./node_modules ./node_modules/mocha/bin/mocha *.js",
"lint": "./node_modules/eslint/bin/eslint.js -c .eslintrc.js *.js"
},
"author": "",
"license": "© Pebble Technology Corp."
}

View file

@ -0,0 +1,50 @@
/**
* 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 */
/* eslint no-console: 0 */
// Constructor to build a mock for the global Pebble object:
module.exports = function() {
const simple = require('simple-mock');
const assert = require('assert');
var eventHandlers = {
appmessage: [],
ready: []
};
simple.mock(this, 'addEventListener', (event_name, handler) => {
assert(event_name in eventHandlers, '\'' + event_name + '\' not known');
eventHandlers[event_name].push(handler);
});
simple.mock(this, 'handleEvent', (event) => {
assert(event.name in eventHandlers, '\'' + event.name + '\' not known');
for (let handler of eventHandlers[event.name]) {
handler(event);
}
});
simple.mock(this, 'sendAppMessage', (msg, complCb, errCb) => {
console.log(
'sendAppMessage: ' + msg + ' complCb: ' + complCb + ' errCb: ' + errCb);
if (complCb) {
complCb(msg);
}
});
};

View file

@ -0,0 +1,457 @@
/**
* 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()
});
});