Skip to content

Add BridgeFake testing support and isAvailable() check#3

Merged
MrPunyapal merged 2 commits into
MrPunyapal:mainfrom
sonalidudhia:feat/bridge-fake-and-availability
Jul 13, 2026
Merged

Add BridgeFake testing support and isAvailable() check#3
MrPunyapal merged 2 commits into
MrPunyapal:mainfrom
sonalidudhia:feat/bridge-fake-and-availability

Conversation

@sonalidudhia

@sonalidudhia sonalidudhia commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Problem

Plugin authors can't develop or test plugin behavior without a compiled mobile app. Plugin::callBridge() resolves nativephp.mobile.bridge from the container and throws RuntimeException when it's absent — so every author ends up hand-rolling the same anonymous-class bridge stub in their tests (see the old tests/PluginTest.php).

Before / after

Before, every plugin's test suite duplicated this:

$bridge = new class {
    public function call(string $function, array $payload): array
    {
        return ['function' => $function, 'payload' => $payload, 'platform' => 'test'];
    }
};

app()->instance('nativephp.mobile.bridge', $bridge);

After:

use {{ namespace }}\Facades\{{ plugin }};

$fake = {{ plugin }}::fake([
    '{{ plugin }}.Example' => ['ok' => true],
]);

{{ plugin }}::example(['message' => 'hi']);

$fake->assertCalled('{{ plugin }}.Example', fn (array $payload) => $payload['message'] === 'hi');

What's added

  • Testing\BridgeFakeHttp::fake()-style canned responses (array or Closure) and call assertions: assertCalled(), assertNotCalled(), assertCalledTimes(), assertNothingCalled(), plus stub(), recorded(), preventStrayCalls().
  • {{ plugin }}::fake(array $responses = []) — binds a BridgeFake into the container in place of the real bridge.
  • isAvailable(): bool on the contract/Plugin/facade — lets app code check whether the bridge is bound before calling into it, so it can no-op gracefully on web/desktop.
  • Stubs (plugin.stub, contract.stub, facade.stub) and docs/testing.md updated to match, per CONTRIBUTING.
  • tests/BridgeFakeTest.php covering all of the above (stubbing, closures, stray-call prevention, every assertion's pass and fail path, isAvailable()).

Design decision — flagging for maintainer input

Unstubbed calls return [] by default; strict mode is opt-in via preventStrayCalls(). This mirrors Http::fake()/Http::preventStrayRequests() semantics rather than throwing by default. Happy to flip the default if you'd rather fail loud out of the box.

Scope

Left tests/PluginTest.php's inline anonymous-class bridge untouched — refactoring it to use BridgeFake felt like a separate, code-review-worthy change on its own, not bundled into this diff.

Verification

Replicated CI exactly: configured the template (php configure.php --vendor=acme --package=mobile-battery --plugin=Battery ...), then ran composer install, composer test (17/17 pass), PHPStan level 8 (clean), Rector dry-run (no diff on any touched file), and confirmed placeholders survive in the unconfigured tree and resolve cleanly in the configured one. Full notes in the PR discussion if useful — one pre-existing note: composer lint's Pint step currently fails on main itself (7 files) because composer.lock isn't committed and a newer laravel/pint resolves with stricter rules than the repo was authored against; my new file matches that same existing style drift rather than introducing a new failure class. Didn't fix it here since it's out of this PR's scope.

Summary by CodeRabbit

  • New Features
    • Added isAvailable() checks to detect whether the mobile bridge is usable.
    • Added a bridge testing fake that stubs responses, records calls, and supports strict mode for unexpected calls.
  • Documentation
    • Documented how to use the bridge fake, stub return payloads, assert calls, and enable strict-mode behavior.
  • Tests
    • Added a comprehensive test suite covering the fake, assertions, default/strict call handling, and isAvailable() semantics.
  • Improvements
    • Improved runtime validation for bridge call support.

Plugin authors currently cannot develop or test plugin behavior without
a compiled mobile app, forcing every author to hand-write an anonymous
class bridge stub. BridgeFake gives Http::fake()-style faking with
stubbed responses and call assertions, and isAvailable() lets app code
gracefully no-op when the bridge isn't bound (web/desktop).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: cd0540c4-1e46-40b1-bc2c-8c2089dcedc3

📥 Commits

Reviewing files that changed from the base of the PR and between feb1b0b and d537ee4.

📒 Files selected for processing (2)
  • src/Plugin.php
  • stubs/plugin.stub
🚧 Files skipped from review as they are similar to previous changes (2)
  • stubs/plugin.stub
  • src/Plugin.php

📝 Walkthrough

Walkthrough

The template adds isAvailable() to plugin contracts and implementations, introduces BridgeFake with stubbing and call assertions, exposes it through the facade, and documents and tests bridge faking and availability behavior.

Changes

Bridge testing and availability

Layer / File(s) Summary
Bridge availability contracts
src/Contracts/{{ plugin }}Contract.php, src/Plugin.php, stubs/*
Contracts and generated plugin templates expose isAvailable() based on the nativephp.mobile.bridge binding and its callable call method.
Bridge fake and facade wiring
src/Testing/BridgeFake.php, src/Facades/{{ plugin }}.php
BridgeFake supports configurable responses, call recording, stray-call rejection, and assertions; the facade binds it into the container through fake().
Fake behavior validation and usage
tests/BridgeFakeTest.php, docs/testing.md
Tests cover responses, closures, call assertions, strict mode, and availability transitions; documentation describes the fake and availability APIs.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant PluginFacade
  participant BridgeFake
  participant LaravelContainer
  participant Plugin
  PluginFacade->>BridgeFake: construct with responses
  PluginFacade->>LaravelContainer: bind fake as nativephp.mobile.bridge
  Plugin->>LaravelContainer: check bridge binding
  LaravelContainer-->>Plugin: return availability
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the main change: adding BridgeFake support and an isAvailable() availability check.
Description check ✅ Passed The description is detailed and covers the problem, solution, design choice, scope, and verification, though it doesn’t follow the template headings exactly.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@sonalidudhia

Copy link
Copy Markdown
Contributor Author

What problem this solves

Plugin talks to native code (Android/iOS) through "bridge." Bridge only exists inside compiled mobile app. On your laptop running composer test, no bridge — code crashes with RuntimeException. So every plugin author write same fake bridge by hand, every time, in every test.

Real example — battery plugin
Say plugin has one function: get battery level.

Contract:

public function level(): array
{
    return $this->callBridge('Battery.Level', []);
}

Old way (before this PR) — hand-roll fake bridge in every test file:

$bridge = new class {
    public function call(string $function, array $payload): array
    {
        return ['level' => 80, 'charging' => false];
    }
};

app()->instance('nativephp.mobile.bridge', $bridge);

expect(Battery::level())->toBe(['level' => 80, 'charging' => false]);

Works, but boilerplate. No way to assert what got called, no way to say "fail if code hits an unstubbed function."

New way:

use Acme\MobileBattery\Facades\Battery;

it('reports the battery level', function () {
    Battery::fake([
        'Battery.Level' => ['level' => 80, 'charging' => false],
    ]);

    expect(Battery::level())->toBe(['level' => 80, 'charging' => false]);

    Battery::fake()->assertCalled('Battery.Level');
});

One line to fake, one line to assert. Same pattern devs already know from Laravel's Http::fake().

App code that degrades gracefully

Say a controller shows battery widget only on mobile:

public function widget()
{
    if (! Battery::isAvailable()) {
        return null; // web/desktop request, no bridge, skip silently
    }

    return Battery::level();
}

No isAvailable() before this PR — that check had to be a try/catch around RuntimeException, ugly.

Steps to use the changed code

In tests, fake bridge before calling plugin methods:
Battery::fake(['Battery.Level' => ['level' => 42]]);

Dynamic response — use closure instead of array, gets payload:

Battery::fake([
    'Battery.Charge' => fn (array $payload) => ['charged_to' => $payload['target']],
]);

Assert calls happened:

$fake = Battery::fake();
Battery::level();
$fake->assertCalled('Battery.Level');
$fake->assertCalledTimes('Battery.Level', 1);
$fake->assertNotCalled('Battery.Charge');
Strict mode — fail if code calls anything not stubbed (catches typos/forgotten stubs):
Battery::fake(['Battery.Level' => ['level' => 42]])->preventStrayCalls();
In app code, guard bridge calls:
if (Battery::isAvailable()) {
    $level = Battery::level();
}

No setup step needed beyond calling fake() — it swaps the container binding for you, real bridge stays untouched outside tests.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/Plugin.php`:
- Around line 39-49: Replace method_exists($bridge, 'call') with
is_callable([$bridge, 'call']) in isAvailable(), and update the corresponding
guard in callBridge() to use the same callability predicate so availability
matches the actual invocation path.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 39172b0c-57cb-47fe-94b0-1ea375ce0fa7

📥 Commits

Reviewing files that changed from the base of the PR and between 1991914 and feb1b0b.

📒 Files selected for processing (9)
  • docs/testing.md
  • src/Contracts/{{ plugin }}Contract.php
  • src/Facades/{{ plugin }}.php
  • src/Plugin.php
  • src/Testing/BridgeFake.php
  • stubs/contract.stub
  • stubs/facade.stub
  • stubs/plugin.stub
  • tests/BridgeFakeTest.php

Comment thread src/Plugin.php
method_exists() returns true for methods that exist but aren't
invokable from the calling scope (e.g. private/protected). Both
isAvailable() and callBridge() now use is_callable([$bridge, 'call'])
so the availability check matches the actual invocation path.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@MrPunyapal MrPunyapal merged commit 0a6db93 into MrPunyapal:main Jul 13, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants