diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
new file mode 100644
index 0000000..0e92130
--- /dev/null
+++ b/.github/workflows/tests.yml
@@ -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@v4
+
+ - 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@v4
+
+ # 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
diff --git a/README.md b/README.md
index 44c783c..4ca34f0 100644
--- a/README.md
+++ b/README.md
@@ -103,3 +103,37 @@ The share sheet automatically detects MIME types for common file formats:
- Uses `UIActivityViewController`
- Supports iPad popover presentation
- Files are shared directly via file URLs
+
+## Testing
+
+The plugin extends the NativePHP testing suite with share-specific helpers, so your app tests can assert share sheet activity without knowing any bridge internals:
+
+```php
+use Native\Mobile\Testing\Native;
+
+it('shares the invite link', function () {
+ Native::test(ShareSheet::class)
+ ->tap('Share link')
+ ->assertSharedUrl('https://example.com/invite/abc');
+});
+
+it('shares the exported report', function () {
+ Native::test(ReportScreen::class)
+ ->tap('Share report')
+ ->assertSharedFile('/storage/app/report.pdf');
+});
+
+it('does not share anything before the button is tapped', function () {
+ Native::test(ReportScreen::class)
+ ->assertNothingShared();
+});
+```
+
+### Helpers
+
+- `assertShared()` — assert something was shared, a URL or a file.
+- `assertSharedUrl(?string $url = null)` — assert a URL was shared, or exactly `$url` when given.
+- `assertSharedFile(?string $filePath = null)` — assert a file was shared, or exactly `$filePath` when given.
+- `assertNothingShared()` — assert neither `Share::url()` nor `Share::file()` was called.
+
+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).
diff --git a/phpunit.xml b/phpunit.xml
new file mode 100644
index 0000000..13ad511
--- /dev/null
+++ b/phpunit.xml
@@ -0,0 +1,13 @@
+
+
+
+
+ tests
+
+
+
diff --git a/src/ShareServiceProvider.php b/src/ShareServiceProvider.php
index 7fcb7e4..f5ec2a6 100644
--- a/src/ShareServiceProvider.php
+++ b/src/ShareServiceProvider.php
@@ -3,7 +3,9 @@
namespace Native\Mobile\Providers;
use Illuminate\Support\ServiceProvider;
+use Native\Mobile\Providers\Testing\ShareMacros;
use Native\Mobile\Share;
+use Native\Mobile\Testing\FakeBridge;
class ShareServiceProvider extends ServiceProvider
{
@@ -12,5 +14,14 @@ public function register(): void
$this->app->singleton(Share::class, function () {
return new Share;
});
+
+ // Test sugar (assertSharedUrl() 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')) {
+ ShareMacros::register();
+ }
}
}
diff --git a/src/Testing/ShareMacros.php b/src/Testing/ShareMacros.php
new file mode 100644
index 0000000..09188f1
--- /dev/null
+++ b/src/Testing/ShareMacros.php
@@ -0,0 +1,87 @@
+tap('Share link')
+ * ->assertSharedUrl('https://example.com/invite/abc');
+ *
+ * Registered by ShareServiceProvider when the app is running unit tests on
+ * a core whose FakeBridge supports macros.
+ */
+class ShareMacros
+{
+ public static function register(): void
+ {
+ /** Assert something was shared — a URL, a file, or either. */
+ FakeBridge::macro('assertShared', function () {
+ $shared = [...$this->callsTo('Share.Url'), ...$this->callsTo('Share.File')];
+
+ Assert::assertNotEmpty($shared, 'Expected the share sheet to be opened, but it was not.');
+
+ return $this;
+ });
+
+ /** Assert a URL was shared — any URL, or exactly $url when given. */
+ FakeBridge::macro('assertSharedUrl', function (?string $url = null) {
+ if ($url === null) {
+ return $this->assertCalled('Share.Url');
+ }
+
+ $shared = array_map(
+ fn (array $call) => $call['params']['url'] ?? '',
+ $this->callsTo('Share.Url')
+ );
+
+ Assert::assertContains(
+ $url,
+ $shared,
+ "Expected [{$url}] to be shared. Shared: "
+ .($shared === [] ? '(nothing)' : '['.implode('], [', $shared).']')
+ );
+
+ return $this;
+ });
+
+ /** Assert a file was shared — any file, or exactly $filePath when given. */
+ FakeBridge::macro('assertSharedFile', function (?string $filePath = null) {
+ if ($filePath === null) {
+ return $this->assertCalled('Share.File');
+ }
+
+ $shared = array_map(
+ fn (array $call) => $call['params']['filePath'] ?? '',
+ $this->callsTo('Share.File')
+ );
+
+ Assert::assertContains(
+ $filePath,
+ $shared,
+ "Expected [{$filePath}] to be shared. Shared: "
+ .($shared === [] ? '(nothing)' : '['.implode('], [', $shared).']')
+ );
+
+ return $this;
+ });
+
+ /** Assert nothing was shared — no Share.Url or Share.File call. */
+ FakeBridge::macro('assertNothingShared', function () {
+ $shared = [...$this->callsTo('Share.Url'), ...$this->callsTo('Share.File')];
+
+ Assert::assertEmpty(
+ $shared,
+ 'Expected nothing to be shared, but the share sheet was opened.'
+ );
+
+ return $this;
+ });
+ }
+}
diff --git a/tests/Pest.php b/tests/Pest.php
new file mode 100644
index 0000000..88a51b7
--- /dev/null
+++ b/tests/Pest.php
@@ -0,0 +1,3 @@
+in('.');
diff --git a/tests/PluginTest.php b/tests/PluginTest.php
new file mode 100644
index 0000000..d5a8e03
--- /dev/null
+++ b/tests/PluginTest.php
@@ -0,0 +1,121 @@
+pluginPath = getenv('PLUGIN_PATH') ?: dirname(__DIR__);
+ $this->manifestPath = $this->pluginPath.'/nativephp.json';
+});
+
+describe('Plugin Manifest', function () {
+ it('has a valid nativephp.json file', function () {
+ expect(file_exists($this->manifestPath))->toBeTrue();
+
+ json_decode(file_get_contents($this->manifestPath), true);
+
+ expect(json_last_error())->toBe(JSON_ERROR_NONE);
+ });
+
+ it('has required fields', function () {
+ $manifest = json_decode(file_get_contents($this->manifestPath), true);
+
+ expect($manifest)->toHaveKeys(['namespace', 'bridge_functions']);
+ expect($manifest['namespace'])->toBe('Share');
+ });
+
+ it('declares every bridge function for both platforms', function () {
+ $manifest = json_decode(file_get_contents($this->manifestPath), true);
+
+ expect($manifest['bridge_functions'])->toBeArray()->not->toBeEmpty();
+
+ $names = array_column($manifest['bridge_functions'], 'name');
+ expect($names)->toContain(
+ 'Share.Url',
+ 'Share.File',
+ );
+
+ foreach ($manifest['bridge_functions'] as $function) {
+ expect($function)->toHaveKeys(['name']);
+ expect(isset($function['android']) || isset($function['ios']))->toBeTrue();
+ }
+ });
+
+ it('declares no events', function () {
+ $manifest = json_decode(file_get_contents($this->manifestPath), true);
+
+ expect($manifest['events'])->toBeArray()->toBeEmpty();
+ });
+
+ it('requests no permissions', function () {
+ $manifest = json_decode(file_get_contents($this->manifestPath), true);
+
+ expect($manifest['android']['permissions'])->toBeArray()->toBeEmpty();
+ expect($manifest['ios']['info_plist'])->toBeArray()->toBeEmpty();
+ });
+});
+
+describe('Native Code', function () {
+ it('has matching bridge function classes in native code', function () {
+ $manifest = json_decode(file_get_contents($this->manifestPath), true);
+
+ $kotlinContent = implode('', array_map('file_get_contents', glob($this->pluginPath.'/resources/android/*.kt')));
+ $swiftContent = implode('', array_map('file_get_contents', glob($this->pluginPath.'/resources/ios/*.swift')));
+
+ foreach ($manifest['bridge_functions'] as $function) {
+ if (isset($function['android'])) {
+ $parts = explode('.', $function['android']);
+ expect($kotlinContent)->toContain('class '.end($parts));
+ }
+
+ if (isset($function['ios'])) {
+ $parts = explode('.', $function['ios']);
+ expect($swiftContent)->toContain('class '.end($parts));
+ }
+ }
+ });
+});
+
+describe('Composer Configuration', function () {
+ it('has valid composer.json', function () {
+ $composer = json_decode(file_get_contents($this->pluginPath.'/composer.json'), true);
+
+ expect(json_last_error())->toBe(JSON_ERROR_NONE);
+ expect($composer['type'])->toBe('nativephp-plugin');
+ expect($composer['require'])->toHaveKey('php');
+ expect($composer['require']['php'])->not->toBeEmpty();
+ expect($composer['require'])->toHaveKey('nativephp/mobile');
+ });
+
+ it('registers a provider that maps to an existing file', function () {
+ $composer = json_decode(file_get_contents($this->pluginPath.'/composer.json'), true);
+
+ $providers = $composer['extra']['laravel']['providers'] ?? [];
+ expect($providers)->not->toBeEmpty();
+
+ foreach ($providers as $provider) {
+ $matched = false;
+
+ foreach ($composer['autoload']['psr-4'] as $prefix => $path) {
+ if (! str_starts_with($provider, $prefix)) {
+ continue;
+ }
+
+ $relative = str_replace('\\', '/', substr($provider, strlen($prefix)));
+ $file = $this->pluginPath.'/'.rtrim($path, '/').'/'.$relative.'.php';
+
+ if (file_exists($file)) {
+ $matched = true;
+ break;
+ }
+ }
+
+ expect($matched)->toBeTrue("Provider {$provider} does not map to a file");
+ }
+ });
+});
diff --git a/tests/ShareBridgeTest.php b/tests/ShareBridgeTest.php
new file mode 100644
index 0000000..4833e3d
--- /dev/null
+++ b/tests/ShareBridgeTest.php
@@ -0,0 +1,311 @@
+bridge = Native::fakeBridge();
+});
+
+describe('url()', function () {
+ it('fires Share.Url with title, text and url keys', function () {
+ (new Share)->url('Check this out', 'You have to see this', 'https://nativephp.com');
+
+ $this->bridge->assertCalled('Share.Url', function (array $p) {
+ expect($p)->toHaveKeys(['title', 'text', 'url']);
+ expect($p['title'])->toBe('Check this out');
+ expect($p['text'])->toBe('You have to see this');
+ expect($p['url'])->toBe('https://nativephp.com');
+
+ return true;
+ });
+ });
+
+ it('does not send a message or filePath key', function () {
+ (new Share)->url('Title', 'Text', 'https://nativephp.com');
+
+ $this->bridge->assertCalled('Share.Url', function (array $p) {
+ expect($p)->not->toHaveKey('message');
+ expect($p)->not->toHaveKey('filePath');
+
+ return true;
+ });
+ });
+
+ it('passes each argument through positionally without transposing them', function () {
+ (new Share)->url('the-title', 'the-text', 'the-url');
+
+ $this->bridge->assertCalled('Share.Url', function (array $p) {
+ expect($p['title'])->toBe('the-title');
+ expect($p['text'])->toBe('the-text');
+ expect($p['url'])->toBe('the-url');
+
+ return true;
+ });
+ });
+
+ it('preserves multiline text with quotes verbatim', function () {
+ $snippet = "\n Hello 'world'\n";
+ (new Share)->url('Title', $snippet, 'https://nativephp.com');
+
+ $this->bridge->assertCalled('Share.Url', function (array $p) use ($snippet) {
+ expect($p['text'])->toBe($snippet);
+
+ return true;
+ });
+ });
+
+ it('preserves special characters and symbols verbatim in the title', function () {
+ $title = 'Save 50% off — "limited time" & more!';
+ (new Share)->url($title, 'Text', 'https://nativephp.com');
+
+ $this->bridge->assertCalled('Share.Url', function (array $p) use ($title) {
+ expect($p['title'])->toBe($title);
+
+ return true;
+ });
+ });
+
+ it('preserves unicode characters and emoji verbatim', function () {
+ $text = 'こんにちは世界 🌍 — café résumé naïve';
+ (new Share)->url('Title', $text, 'https://nativephp.com');
+
+ $this->bridge->assertCalled('Share.Url', function (array $p) use ($text) {
+ expect($p['text'])->toBe($text);
+
+ return true;
+ });
+ });
+
+ it('preserves query strings and fragments in the url verbatim', function () {
+ $url = 'https://nativephp.com/docs?ref=share&utm_source=app#install';
+ (new Share)->url('Title', 'Text', $url);
+
+ $this->bridge->assertCalled('Share.Url', function (array $p) use ($url) {
+ expect($p['url'])->toBe($url);
+
+ return true;
+ });
+ });
+
+ it('passes through empty strings for title and text', function () {
+ (new Share)->url('', '', 'https://nativephp.com');
+
+ $this->bridge->assertCalled('Share.Url', function (array $p) {
+ expect($p['title'])->toBe('');
+ expect($p['text'])->toBe('');
+ expect($p['url'])->toBe('https://nativephp.com');
+
+ return true;
+ });
+ });
+
+ it('returns void', function () {
+ $result = (new Share)->url('Title', 'Text', 'https://nativephp.com');
+
+ expect($result)->toBeNull();
+ });
+
+ it('fires Share.Url exactly once per call', function () {
+ (new Share)->url('Title', 'Text', 'https://nativephp.com');
+
+ $this->bridge->assertCalledTimes('Share.Url', 1);
+ });
+
+ it('does not fire Share.File', function () {
+ (new Share)->url('Title', 'Text', 'https://nativephp.com');
+
+ $this->bridge->assertNotCalled('Share.File');
+ });
+
+ it('records each call independently across multiple invocations', function () {
+ (new Share)->url('First', 'First text', 'https://nativephp.com/first');
+ (new Share)->url('Second', 'Second text', 'https://nativephp.com/second');
+
+ $calls = $this->bridge->callsTo('Share.Url');
+ expect($calls)->toHaveCount(2);
+ expect($calls[0]['params']['title'])->toBe('First');
+ expect($calls[0]['params']['url'])->toBe('https://nativephp.com/first');
+ expect($calls[1]['params']['title'])->toBe('Second');
+ expect($calls[1]['params']['url'])->toBe('https://nativephp.com/second');
+ });
+});
+
+describe('file()', function () {
+ it('fires Share.File with title, message and filePath keys', function () {
+ (new Share)->file('Vacation photo', 'Look at this!', '/storage/app/photo.jpg');
+
+ $this->bridge->assertCalled('Share.File', function (array $p) {
+ expect($p)->toHaveKeys(['title', 'message', 'filePath']);
+ expect($p['title'])->toBe('Vacation photo');
+ expect($p['message'])->toBe('Look at this!');
+ expect($p['filePath'])->toBe('/storage/app/photo.jpg');
+
+ return true;
+ });
+ });
+
+ it('sends the text argument under the message key, not text', function () {
+ (new Share)->file('Title', 'the shared text', '/storage/app/file.pdf');
+
+ $this->bridge->assertCalled('Share.File', function (array $p) {
+ expect($p)->not->toHaveKey('text');
+ expect($p['message'])->toBe('the shared text');
+
+ return true;
+ });
+ });
+
+ it('does not send a url key', function () {
+ (new Share)->file('Title', 'Text', '/storage/app/file.pdf');
+
+ $this->bridge->assertCalled('Share.File', function (array $p) {
+ expect($p)->not->toHaveKey('url');
+
+ return true;
+ });
+ });
+
+ it('passes each argument through positionally without transposing them', function () {
+ (new Share)->file('the-title', 'the-message', '/the/file/path.txt');
+
+ $this->bridge->assertCalled('Share.File', function (array $p) {
+ expect($p['title'])->toBe('the-title');
+ expect($p['message'])->toBe('the-message');
+ expect($p['filePath'])->toBe('/the/file/path.txt');
+
+ return true;
+ });
+ });
+
+ it('preserves multiline messages with quotes verbatim', function () {
+ $snippet = "\n Hello 'world'\n";
+ (new Share)->file('Title', $snippet, '/storage/app/file.txt');
+
+ $this->bridge->assertCalled('Share.File', function (array $p) use ($snippet) {
+ expect($p['message'])->toBe($snippet);
+
+ return true;
+ });
+ });
+
+ it('preserves special characters and symbols verbatim in the title', function () {
+ $title = 'Save 50% off — "limited time" & more!';
+ (new Share)->file($title, 'Message', '/storage/app/file.txt');
+
+ $this->bridge->assertCalled('Share.File', function (array $p) use ($title) {
+ expect($p['title'])->toBe($title);
+
+ return true;
+ });
+ });
+
+ it('preserves unicode characters and emoji verbatim', function () {
+ $message = 'こんにちは世界 🌍 — café résumé naïve';
+ (new Share)->file('Title', $message, '/storage/app/file.txt');
+
+ $this->bridge->assertCalled('Share.File', function (array $p) use ($message) {
+ expect($p['message'])->toBe($message);
+
+ return true;
+ });
+ });
+
+ it('preserves file paths with spaces and special characters verbatim', function () {
+ $filePath = '/storage/app/My Documents/report (final) [v2].pdf';
+ (new Share)->file('Title', 'Message', $filePath);
+
+ $this->bridge->assertCalled('Share.File', function (array $p) use ($filePath) {
+ expect($p['filePath'])->toBe($filePath);
+
+ return true;
+ });
+ });
+
+ it('passes through empty strings for title and message', function () {
+ (new Share)->file('', '', '/storage/app/file.txt');
+
+ $this->bridge->assertCalled('Share.File', function (array $p) {
+ expect($p['title'])->toBe('');
+ expect($p['message'])->toBe('');
+ expect($p['filePath'])->toBe('/storage/app/file.txt');
+
+ return true;
+ });
+ });
+
+ it('returns void', function () {
+ $result = (new Share)->file('Title', 'Message', '/storage/app/file.txt');
+
+ expect($result)->toBeNull();
+ });
+
+ it('fires Share.File exactly once per call', function () {
+ (new Share)->file('Title', 'Message', '/storage/app/file.txt');
+
+ $this->bridge->assertCalledTimes('Share.File', 1);
+ });
+
+ it('does not fire Share.Url', function () {
+ (new Share)->file('Title', 'Message', '/storage/app/file.txt');
+
+ $this->bridge->assertNotCalled('Share.Url');
+ });
+
+ it('records each call independently across multiple invocations', function () {
+ (new Share)->file('First', 'First message', '/storage/app/first.txt');
+ (new Share)->file('Second', 'Second message', '/storage/app/second.txt');
+
+ $calls = $this->bridge->callsTo('Share.File');
+ expect($calls)->toHaveCount(2);
+ expect($calls[0]['params']['title'])->toBe('First');
+ expect($calls[0]['params']['filePath'])->toBe('/storage/app/first.txt');
+ expect($calls[1]['params']['title'])->toBe('Second');
+ expect($calls[1]['params']['filePath'])->toBe('/storage/app/second.txt');
+ });
+});
+
+describe('url() and file() together', function () {
+ it('fires nothing before either method is called', function () {
+ $this->bridge->assertNothingCalled();
+ });
+
+ it('calls Share.Url then Share.File in the order invoked', function () {
+ (new Share)->url('Title', 'Text', 'https://nativephp.com');
+ (new Share)->file('Title', 'Message', '/storage/app/file.txt');
+
+ $this->bridge->assertCallOrder(['Share.Url', 'Share.File']);
+ });
+
+ it('keeps the two bridge functions and their payloads independent', function () {
+ (new Share)->url('URL title', 'URL text', 'https://nativephp.com');
+ (new Share)->file('File title', 'File message', '/storage/app/file.txt');
+
+ $this->bridge->assertCalled('Share.Url', function (array $p) {
+ expect($p['title'])->toBe('URL title');
+ expect($p)->not->toHaveKey('filePath');
+
+ return true;
+ });
+
+ $this->bridge->assertCalled('Share.File', function (array $p) {
+ expect($p['title'])->toBe('File title');
+ expect($p)->not->toHaveKey('url');
+
+ return true;
+ });
+ });
+});
diff --git a/tests/ShareMacrosTest.php b/tests/ShareMacrosTest.php
new file mode 100644
index 0000000..c73bf2a
--- /dev/null
+++ b/tests/ShareMacrosTest.php
@@ -0,0 +1,132 @@
+markTestSkipped('This core\'s FakeBridge does not support macros.');
+ }
+
+ $this->bridge = Native::fakeBridge();
+});
+
+describe('assertShared()', function () {
+ it('passes when a URL was shared', function () {
+ (new Share)->url('Title', 'Text', 'https://nativephp.com');
+
+ $this->bridge->assertShared();
+ });
+
+ it('passes when a file was shared', function () {
+ (new Share)->file('Title', 'Message', '/storage/app/file.txt');
+
+ $this->bridge->assertShared();
+ });
+
+ it('fails when nothing was shared', function () {
+ expect(fn () => $this->bridge->assertShared())
+ ->toThrow(AssertionFailedError::class);
+ });
+});
+
+describe('assertSharedUrl()', function () {
+ it('passes when any URL was shared', function () {
+ (new Share)->url('Title', 'Text', 'https://nativephp.com');
+
+ $this->bridge->assertSharedUrl();
+ });
+
+ it('matches the exact shared url', function () {
+ (new Share)->url('First', 'Text', 'https://nativephp.com/first');
+ (new Share)->url('Second', 'Text', 'https://nativephp.com/second');
+
+ $this->bridge->assertSharedUrl('https://nativephp.com/second');
+ });
+
+ it('fails when nothing was shared', function () {
+ expect(fn () => $this->bridge->assertSharedUrl())
+ ->toThrow(AssertionFailedError::class);
+ });
+
+ it('fails when a different url was shared, naming what was', function () {
+ (new Share)->url('Title', 'Text', 'https://nativephp.com/actual');
+
+ expect(fn () => $this->bridge->assertSharedUrl('https://nativephp.com/expected'))
+ ->toThrow(AssertionFailedError::class, 'https://nativephp.com/actual');
+ });
+
+ it('fails when only a file was shared', function () {
+ (new Share)->file('Title', 'Message', '/storage/app/file.txt');
+
+ expect(fn () => $this->bridge->assertSharedUrl())
+ ->toThrow(AssertionFailedError::class);
+ });
+});
+
+describe('assertSharedFile()', function () {
+ it('passes when any file was shared', function () {
+ (new Share)->file('Title', 'Message', '/storage/app/file.txt');
+
+ $this->bridge->assertSharedFile();
+ });
+
+ it('matches the exact shared filePath', function () {
+ (new Share)->file('First', 'Message', '/storage/app/first.txt');
+ (new Share)->file('Second', 'Message', '/storage/app/second.txt');
+
+ $this->bridge->assertSharedFile('/storage/app/second.txt');
+ });
+
+ it('fails when nothing was shared', function () {
+ expect(fn () => $this->bridge->assertSharedFile())
+ ->toThrow(AssertionFailedError::class);
+ });
+
+ it('fails when a different file was shared, naming what was', function () {
+ (new Share)->file('Title', 'Message', '/storage/app/actual.txt');
+
+ expect(fn () => $this->bridge->assertSharedFile('/storage/app/expected.txt'))
+ ->toThrow(AssertionFailedError::class, '/storage/app/actual.txt');
+ });
+
+ it('fails when only a url was shared', function () {
+ (new Share)->url('Title', 'Text', 'https://nativephp.com');
+
+ expect(fn () => $this->bridge->assertSharedFile())
+ ->toThrow(AssertionFailedError::class);
+ });
+});
+
+describe('assertNothingShared()', function () {
+ it('passes when nothing was shared', function () {
+ $this->bridge->assertNothingShared();
+ });
+
+ it('fails after a url share', function () {
+ (new Share)->url('Title', 'Text', 'https://nativephp.com');
+
+ expect(fn () => $this->bridge->assertNothingShared())
+ ->toThrow(AssertionFailedError::class);
+ });
+
+ it('fails after a file share', function () {
+ (new Share)->file('Title', 'Message', '/storage/app/file.txt');
+
+ expect(fn () => $this->bridge->assertNothingShared())
+ ->toThrow(AssertionFailedError::class);
+ });
+});
diff --git a/tests/TestCase.php b/tests/TestCase.php
new file mode 100644
index 0000000..a348c14
--- /dev/null
+++ b/tests/TestCase.php
@@ -0,0 +1,32 @@
+set('nativephp.app_id', 'com.test.app');
+ $app['config']->set('nativephp.version', '1.0.0');
+ $app['config']->set('nativephp.version_code', 1);
+ $app['config']->set('app.name', 'Test App');
+ }
+}