Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions .github/workflows/nodejs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,70 @@ jobs:
- name: Build all TS apps and widgets
working-directory: ./typescript
run: npm run build

functional-tests:
runs-on: ubuntu-latest

steps:
- name: Checkout repository and submodules
uses: actions/checkout@v3
with:
submodules: recursive
fetch-depth: 0

- name: Use Node.js 18.x
uses: actions/setup-node@v3
with:
node-version: 18.x

- name: Clone EspruinoWebIDE (pinned)
run: |
git clone --depth 1 https://github.com/espruino/EspruinoWebIDE ../EspruinoWebIDE
cd ../EspruinoWebIDE
git fetch --depth 1 origin a62beceec317f6744e58aaa2d1a19447d9458035
git checkout a62beceec317f6744e58aaa2d1a19447d9458035

- name: Detect changed apps
id: detect-apps
run: |
set -e # Exit on error, but handle grep specially
if [ "${{ github.event_name }}" = "pull_request" ]; then
# PR: test only changed apps
CHANGED=$(git diff --name-only ${{ github.event.pull_request.base.sha }}..HEAD)
APPS=$(echo "$CHANGED" | grep '^apps/' | cut -d'/' -f2 | sort -u) || APPS=""
elif [ "${{ github.ref }}" = "refs/heads/master" ]; then
# Push to master: test all apps
APPS=$(ls apps/*/test.json 2>/dev/null | cut -d'/' -f2) || APPS=""
else
# Push to other branches: test only changed apps since master
CHANGED=$(git diff --name-only origin/master...HEAD)
APPS=$(echo "$CHANGED" | grep '^apps/' | cut -d'/' -f2 | sort -u) || APPS=""
fi
# Filter to apps with test.json
TESTABLE=""
for app in $APPS; do
if [ -f "apps/$app/test.json" ]; then
TESTABLE="$TESTABLE $app"
fi
done
echo "apps=$TESTABLE" >> $GITHUB_OUTPUT
echo "Detected testable apps:$TESTABLE"

- name: Run app tests
run: |
APPS="${{ steps.detect-apps.outputs.apps }}"
if [ -z "$APPS" ]; then
echo "No apps with tests to run"
exit 0
fi
FAILED=0
for app in $APPS; do
echo ""
echo "========================================="
echo "Testing $app..."
echo "========================================="
if ! node bin/runapptests.js --id "$app" --verbose; then
FAILED=1
fi
done
exit $FAILED
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ You can also take multiple screenshots and use a website like [this](https://ezg

## Testing

For functional testing of apps using the Bangle.js emulator, see [TESTING.md](TESTING.md).

### Online

This is the best way to test...
Expand Down
116 changes: 116 additions & 0 deletions TESTING.md
Comment thread
bobrippling marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# BangleApps Testing Documentation

This document describes how to write and run functional tests for Bangle.js apps using the emulator-based test runner.

## Running Tests

### Prerequisites

Clone the EspruinoWebIDE at the same level as BangleApps:

```bash
cd ..
git clone https://github.com/espruino/EspruinoWebIDE
```

### Running All Tests

```bash
node bin/runapptests.js --verbose
```

### Running Tests for a Specific App

```bash
node bin/runapptests.js --id android --verbose
```

## Writing Tests

Create a `test.json` file in your app's directory (e.g., `apps/myapp/test.json`).

### Basic Structure

```json
{
"app": "myapp",
"setup": [
{
"id": "default",
"steps": [
{"t": "cmd", "js": "/* setup code */"}
]
}
],
"tests": [
{
"description": "Test description",
"steps": [
{"t": "setup", "id": "default"},
{"t": "assert", "js": "true", "is": "truthy"}
]
}
]
}
```

For available step types and assert conditions, see the inline documentation in `bin/runapptests.js`.

### Example: Testing GPS Power Management

```json
{
"app": "android",
"setup": [{
"id": "default",
"steps": [
{"t": "cmd", "js": "Bangle.setGPSPower = (on, id) => { /* mock */ }"},
{"t": "wrap", "fn": "Bangle.setGPSPower", "id": "gpspower"},
{"t": "cmd", "js": "eval(require('Storage').read('android.boot.js'))"}
]
}],
"tests": [{
"description": "Check GPS power is managed correctly",
"steps": [
{"t": "setup", "id": "default"},
{"t": "assert", "js": "Bangle.setGPSPower(1, 'test')", "is": "truthy"},
{"t": "assertCall", "id": "gpspower", "count": 1}
]
}]
}
```

## CI Behavior

Tests run automatically in GitHub Actions:

- **On Pull Requests**: Only apps with changed files AND a `test.json` are tested
- **On Push to master**: All apps with `test.json` are tested

### Test Results

- **SUCCESS**: All assertions passed
- **FAILURE**: One or more assertions failed
- **TIMEOUT**: Test took longer than 60 seconds (possible infinite loop)

## Troubleshooting

### "You need to git clone EspruinoWebIDE"

The emulator dependency is missing. Run:

```bash
cd ..
git clone https://github.com/espruino/EspruinoWebIDE
```

### Test hangs indefinitely

Tests now have a 60-second timeout. If your test needs longer:
1. Check for infinite loops in your code
2. Check that promises are resolving
3. Consider breaking into smaller tests

### Memory tests are flaky

Memory comparison tests (`saveMemoryUsage`/`checkMemoryUsage`) can be sensitive to GC timing. Consider using tolerance or avoiding exact memory comparisons.
1 change: 1 addition & 0 deletions apps/agenda/ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@
0.17: Fixed "Today" and "Tomorrow" labels displaying in non-current weeks
0.18: Correct date in clockinfo for all-day events in negative timezones
0.19: Change clockinfo title truncation to preserve images
0.20: Add back button support to "No events" screen
1 change: 1 addition & 0 deletions apps/agenda/agenda.js
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ function showList() {
}
if(CALENDAR.length == 0) {
E.showMessage(/*LANG*/"No events");
Bangle.setUI({mode: "custom", back: load});
return;
}
E.showScroller({
Expand Down
2 changes: 1 addition & 1 deletion apps/agenda/metadata.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"id": "agenda",
"name": "Agenda",
"version": "0.19",
"version": "0.20",
"description": "Simple agenda",
"icon": "agenda.png",
"screenshots": [{"url":"screenshot_agenda_overview.png"}, {"url":"screenshot_agenda_event1.png"}, {"url":"screenshot_agenda_event2.png"}],
Expand Down
3 changes: 2 additions & 1 deletion apps/messagesoverlay/ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@
0.09: Fix scrolling to last line for long text
0.10: Track Listeners added with prependListener
Handle changed internal callback variable name for watches introduced in 2v21.104
0.11: Update for new setLCDOverlay remove handler
0.11: Update for new setLCDOverlay remove handler
0.12: Fix premature handler restoration when overlay is updated during message display
15 changes: 14 additions & 1 deletion apps/messagesoverlay/lib.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,26 @@ const isQuiet = function(){
let eventQueue = [];
let callInProgress = false;
let buzzing = false;
let overlayShowing = false;

const show = function(){
let img = ovr.asImage();
LOG("show", img.bpp);
if (ovr.getBPP() == 1) {
img.palette = new Uint16Array([g.theme.fg,g.theme.bg]);
}
Bangle.setLCDOverlay(img, ovrx, ovry, {id:"messagesoverlay", remove:cleanup});
// Only register remove callback on first show to avoid premature cleanup
// when setLCDOverlay replaces existing overlay with same ID
const opts = {id:"messagesoverlay"};
const isFirstShow = !overlayShowing;
if (isFirstShow) {
opts.remove = cleanup;
}
Bangle.setLCDOverlay(img, ovrx, ovry, opts);
// Set flag after successful call to prevent state corruption if setLCDOverlay throws
if (isFirstShow) {
overlayShowing = true;
}
};

const manageEvent = function(event) {
Expand Down Expand Up @@ -620,6 +632,7 @@ const cleanup = function(){

Bangle.setLCDOverlay(undefined, {id: "messagesoverlay"});
ovr = undefined;
overlayShowing = false;
};

const backup = {};
Expand Down
2 changes: 1 addition & 1 deletion apps/messagesoverlay/metadata.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"id": "messagesoverlay",
"name": "Messages Overlay",
"version": "0.11",
"version": "0.12",
"description": "An overlay based implementation of a messages UI (display notifications from iOS and Gadgetbridge/Android)",
"icon": "app.png",
"type": "bootloader",
Expand Down
28 changes: 21 additions & 7 deletions apps/messagesoverlay/test.json
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
{
"app" : "messagesoverlay",
"tests" : [{
"description": "Test handler backgrounding",
"description": "Test handler backgrounding",
"steps" : [
{"t":"upload", "file": "modules/widget_utils.js", "as": "widget_utils"},
{"t":"cmd", "js": "Bangle.loadWidgets()", "text": "Load widgets"},
{"t":"assertArray", "js": "Bangle['#onswipe']", "is":"undefinedOrEmpty", "text": "No swipe handlers"},
{"t":"cmd", "js": "require('widget_utils').swipeOn(0)", "text": "Store widgets in overlay"},
{"t":"assert", "js": "Bangle['#onswipe']", "is":"function", "text": "One swipe handler for widgets"},
{"t":"assert", "js": "require('widget_utils').swipeHandler", "is":"function", "text": "Swipe handler registered on widget_utils"},
Comment thread
bobrippling marked this conversation as resolved.
{"t":"emit", "event":"swipe", "paramsArray": [ 0, 1 ], "text": "Show widgets"},
{"t":"assert", "js": "Bangle['#onswipe']", "is":"function", "text": "One swipe handler for widgets"},
{"t":"assert", "js": "require('widget_utils').swipeHandler", "is":"function", "text": "Swipe handler still registered"},
{"t":"cmd", "js": "require('messagesoverlay').message('text', {src:'Messenger',t:'add',type:'text',id:Date.now().toFixed(0),title:'title',body:'body'})", "text": "Show a message overlay"},
{"t":"assertArray", "js": "Bangle['#onswipe']", "is":"undefinedOrEmpty", "text": "No swipe handlers while message overlay is on screen"},
{"t":"emit", "event":"touch", "paramsArray": [ 1, { "x": 10, "y": 10, "type": 0 } ], "text": "Close message"},
{"t":"assert", "js": "Bangle['#onswipe']", "is":"function", "text": "One swipe handler restored"}
{"t":"assert", "js": "require('widget_utils').swipeHandler", "is":"function", "text": "Swipe handler restored on widget_utils"}
]
},{
"description": "Test swipe handler backgrounding with fastloading (setUI)",
"description": "Test swipe handler backgrounding with fastloading (setUI)",
"steps" : [
{"t":"cmd", "js": "Bangle.on('swipe',print)", "text": "Create listener for swipes"},
{"t":"cmd", "js": "Bangle.setUI({mode: 'clock',remove: ()=>{}})", "text": "Init UI for clock"},
Expand All @@ -25,10 +25,24 @@
{"t":"cmd", "js": "Bangle.setUI()", "text": "Trigger removal of UI"},
{"t":"assertArray", "js": "Bangle['#onswipe']", "is":"undefinedOrEmpty", "text": "Still no swipe handlers"},
{"t":"emit", "event":"touch", "paramsArray": [ 1, { "x": 10, "y": 10, "type": 0 } ], "text": "Close message"},
{"t":"assert", "js": "Bangle['#onswipe']", "is":"function", "text": "One swipe handler restored"}
{"t":"assert", "js": "Bangle['#onswipe']", "is":"truthy", "text": "Swipe handler restored"}
]
},{
"description": "Test watch backgrounding",
"description": "Test overlay update does not trigger premature cleanup",
"steps" : [
{"t":"cmd", "js": "Bangle.on('swipe',print)", "text": "Create listener for swipes"},
{"t":"assert", "js": "Bangle['#onswipe']", "is":"truthy", "text": "Swipe handler registered"},
{"t":"cmd", "js": "require('messagesoverlay').message('text', {src:'Test',t:'add',type:'text',id:'msg1',title:'First',body:'Message 1'})", "text": "Show first message"},
{"t":"assertArray", "js": "Bangle['#onswipe']", "is":"undefinedOrEmpty", "text": "Handlers backgrounded after first message"},
{"t":"cmd", "js": "require('messagesoverlay').message('text', {src:'Test',t:'add',type:'text',id:'msg2',title:'Second',body:'Message 2'})", "text": "Show second message (triggers overlay update)"},
{"t":"assertArray", "js": "Bangle['#onswipe']", "is":"undefinedOrEmpty", "text": "Handlers still backgrounded after overlay update"},
{"t":"emit", "event":"touch", "paramsArray": [ 1, { "x": 10, "y": 10, "type": 0 } ], "text": "Close first message"},
{"t":"assertArray", "js": "Bangle['#onswipe']", "is":"undefinedOrEmpty", "text": "Handlers still backgrounded with message in queue"},
{"t":"emit", "event":"touch", "paramsArray": [ 1, { "x": 10, "y": 10, "type": 0 } ], "text": "Close second message"},
{"t":"assert", "js": "Bangle['#onswipe']", "is":"truthy", "text": "Handler restored after all messages closed"}
]
},{
"description": "Test watch backgrounding",
"steps" : [
{"t":"cmd", "js": "setWatch(print,BTN)", "text": "Create watch"},
{"t":"cmd", "js": "require('messagesoverlay').message('text', {src:'Messenger',t:'add',type:'text',id:Date.now().toFixed(0),title:'title',body:'body'})", "text": "Show a message overlay"},
Expand Down
Loading