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
63 changes: 63 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
name: tests

on:
push:
branches: [main]
pull_request:
branches: [main]

permissions:
contents: read

jobs:
pest:
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v5

- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.4'
coverage: none
tools: composer:v2

# nativephp/mobile v4 is not a stable Packagist release yet, and this
# suite needs its FakeBridge testing classes. Resolve the core from the
# public mobile-air repo's main branch (dev-main) — the same override
# test-plugins.sh applies locally. The default GITHUB_TOKEN is used only
# to avoid GitHub API rate limits while reading the public repo.
- name: Point Composer at the v4 core (dev-main)
run: |
composer config repositories.nativephp-mobile vcs https://github.com/nativephp/mobile-air
composer config minimum-stability dev
composer config prefer-stable true
composer config github-oauth.github.com "${{ secrets.GITHUB_TOKEN }}"
composer require "nativephp/mobile:dev-main" --no-update --no-interaction

- name: Install dependencies
run: composer update --prefer-dist --no-interaction --no-progress

- name: Run Pest
run: vendor/bin/pest

pint:
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v5

# Pint is a standalone formatter — it needs no project vendor/, so this
# job skips composer entirely.
- name: Setup PHP with Pint
uses: shivammathur/setup-php@v2
with:
php-version: '8.4'
coverage: none
tools: pint

- name: Check code style
run: pint --test
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,39 @@ useEffect(() => {
}, []);
```

## Testing

The plugin extends the NativePHP testing suite with microphone-specific helpers, so your app tests can fake recordings and assert recording activity without knowing any bridge internals:

```php
use Native\Mobile\Testing\Native;

it('saves a voice memo', function () {
Native::fakeBridge()->withRecording('/storage/emulated/0/audio/note.m4a');

Native::test(VoiceMemo::class)
->tap('Record')
->assertRecordingStarted();
});

it('shows the paused state in the UI', function () {
Native::fakeBridge()->withMicrophoneStatus('paused');

Native::test(VoiceMemo::class)
->assertSet('status', 'paused');
});
```

### Helpers

- `withRecording(?string $path = null)` — fake a completed recording. `getStatus()` reports `"idle"` and `getRecording()` reports `$path` (a generic `.m4a` path when omitted) — the state right after a real recording session ends.
- `withMicrophoneStatus(string $status)` — script `getStatus()` directly (e.g. `"recording"` or `"paused"`), without a completed recording on disk.
- `assertRecordingStarted()` — assert a recording was started.
- `assertRecordingStopped()` — assert the recording was stopped.
- `assertNothingRecorded()` — assert no recording was ever started.

The helpers are available on `Native::fakeBridge()` and chain directly off `Native::test(...)`. They register automatically while running tests (requires a core with a macroable FakeBridge; on older cores they simply don't register).

## Notes

- **Microphone Permission:** The first time your app requests microphone access, users will be prompted for permission. If denied, recording functions will fail silently.
Expand Down
13 changes: 13 additions & 0 deletions phpunit.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="vendor/autoload.php"
colors="true"
cacheDirectory=".phpunit.cache"
>
<testsuites>
<testsuite name="Plugin">
<directory>tests</directory>
</testsuite>
</testsuites>
</phpunit>
13 changes: 12 additions & 1 deletion src/MicrophoneServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

use Illuminate\Support\ServiceProvider;
use Native\Mobile\Microphone;
use Native\Mobile\Providers\Testing\MicrophoneMacros;
use Native\Mobile\Testing\FakeBridge;

class MicrophoneServiceProvider extends ServiceProvider
{
Expand All @@ -12,5 +14,14 @@ public function register(): void
$this->app->singleton(Microphone::class, function () {
return new Microphone;
});

// Test sugar (assertRecordingStarted() etc.) — only under a test
// runner, and only on a core whose FakeBridge is macroable (the
// method_exists guard keeps older v4 and v3 cores fatal-free).
if ($this->app->runningUnitTests()
&& class_exists(FakeBridge::class)
&& method_exists(FakeBridge::class, 'macro')) {
MicrophoneMacros::register();
}
}
}
}
66 changes: 66 additions & 0 deletions src/Testing/MicrophoneMacros.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php

namespace Native\Mobile\Providers\Testing;

use Native\Mobile\Testing\FakeBridge;

/**
* Microphone test vocabulary for the NativePHP testing suite, registered as
* FakeBridge macros so app tests read in recording terms instead of raw
* bridge method strings:
*
* Native::fakeBridge()->withRecording('/storage/emulated/0/audio/note.m4a');
*
* Native::test(VoiceMemo::class)
* ->tap('record')
* ->assertRecordingStarted();
*
* Registered by MicrophoneServiceProvider when the app is running unit
* tests on a core whose FakeBridge supports macros.
*/
class MicrophoneMacros
{
public static function register(): void
{
/**
* Fake a completed recording: Microphone.GetStatus reports "idle"
* (recording finished, nothing in progress) and
* Microphone.GetRecording reports $path — so getStatus() and
* getRecording() read exactly as they would right after a real
* recording session ends. $path defaults to a generic .m4a path
* when omitted.
*/
FakeBridge::macro('withRecording', function (?string $path = null) {
$path ??= 'recording.m4a';

$this->respondTo('Microphone.GetStatus', ['status' => 'idle']);

return $this->respondTo('Microphone.GetRecording', ['path' => $path]);
});

/**
* Script Microphone.GetStatus directly — for asserting on
* "recording" or "paused" states without a completed recording on
* disk. Use withRecording() instead when the test also needs
* getRecording() to resolve a path.
*/
FakeBridge::macro('withMicrophoneStatus', function (string $status) {
return $this->respondTo('Microphone.GetStatus', ['status' => $status]);
});

/** Assert a recording was started (record()->start(), explicit or via auto-start). */
FakeBridge::macro('assertRecordingStarted', function () {
return $this->assertCalled('Microphone.Start');
});

/** Assert the recording was stopped. */
FakeBridge::macro('assertRecordingStopped', function () {
return $this->assertCalled('Microphone.Stop');
});

/** Assert no recording was ever started. */
FakeBridge::macro('assertNothingRecorded', function () {
return $this->assertNotCalled('Microphone.Start');
});
}
}
Loading