diff --git a/README.md b/README.md
index 2570e37..085a870 100644
--- a/README.md
+++ b/README.md
@@ -362,7 +362,56 @@ Pass `BrowserOptions`, `ImageOptions`, or `PdfOptions` objects through the `brow
### Custom Drivers
-Chrome is the built-in driver. To supply your own, implement [`Driver`](src/Contracts/Driver.php) and bind it in your service provider's `register()` method, or pass it to the standalone `Screenshot` constructor. Drivers supporting `close()` should also implement [`ManagesBrowsers`](src/Contracts/ManagesBrowsers.php).
+The facade uses `ScreenshotManager` to resolve named drivers. Chrome is included as `chrome` and is the default. Calls such as `Screenshot::html()` are forwarded to the configured default driver:
+
+```php
+Screenshot::html('
Hello!
')->capture();
+Screenshot::driver('chrome')->html('Hello!
')->capture();
+```
+
+To register another driver, implement [`Driver`](src/Contracts/Driver.php) and call `extend()` in your service provider's `boot()` method:
+
+```php
+use App\Screenshots\RemoteDriver;
+use DirectoryTree\Screenshot\Facades\Screenshot;
+use Illuminate\Contracts\Foundation\Application;
+
+public function boot(): void
+{
+ Screenshot::extend('remote', function (Application $app) {
+ return $app->make(RemoteDriver::class);
+ });
+}
+```
+
+Select it for an individual capture:
+
+```php
+Screenshot::driver('remote')->url('https://example.com')->pdf()->capture();
+```
+
+Set `SCREENSHOT_DRIVER=remote` or `screenshot.default` to use it by default. Drivers are created on first use and cached by name. Every driver uses the same fluent builders and configured capture, image, and PDF defaults; custom drivers only implement the capture operation.
+
+Inject `DirectoryTree\Screenshot\Contracts\Factory` when using the container directly. Both the manager and its fake implement this contract:
+
+```php
+use DirectoryTree\Screenshot\Contracts\Factory;
+use DirectoryTree\Screenshot\ImageResult;
+
+class InvoiceScreenshot
+{
+ public function __construct(
+ protected Factory $screenshots,
+ ) {}
+
+ public function capture(string $html): ImageResult
+ {
+ return $this->screenshots->driver()->html($html)->capture();
+ }
+}
+```
+
+Outside Laravel, pass a driver to the standalone `Screenshot` constructor. Drivers supporting `close()` should also implement [`ManagesBrowsers`](src/Contracts/ManagesBrowsers.php).
Drivers receive `SourceType::Html` or `SourceType::Url` and handle every output through `capture(string $source, SourceType $type, CaptureOptions $options)`. Blade views are rendered to HTML first. `CaptureOptions` contains:
@@ -393,6 +442,10 @@ Screenshot::assertCaptured(function (string $source, SourceType $type, CaptureOp
});
```
+`Screenshot::fake()` intercepts both the default driver and explicit `driver()` selections without creating real drivers. Assertions collect captures and shutdown requests across all driver names.
+
+`Screenshot::fake()` also replaces the `Contracts\Factory` container binding. Services resolved after faking receive the same recorder as the facade. Use this contract for constructor injection; the fake does not extend the concrete `ScreenshotManager` class.
+
`assertCaptured()` and `assertCapturedCount()` include PNG, JPEG, and PDF captures. Use `assertNothingCaptured()` when no captures should occur. Assert browser shutdown requests with `assertClosed($key)`, `assertNotClosed($key)`, and `assertNothingClosed()`.
Check the output format in the callback when it matters:
diff --git a/config/screenshot.php b/config/screenshot.php
index 233e2be..9360982 100644
--- a/config/screenshot.php
+++ b/config/screenshot.php
@@ -2,6 +2,18 @@
return [
+ /*
+ |--------------------------------------------------------------------------
+ | Default Driver
+ |--------------------------------------------------------------------------
+ |
+ | Captures use this driver unless one is selected explicitly. Chrome is
+ | included by default. Register custom drivers using Screenshot::extend().
+ |
+ */
+
+ 'default' => env('SCREENSHOT_DRIVER', 'chrome'),
+
/*
|--------------------------------------------------------------------------
| Chrome / Chromium
diff --git a/src/Contracts/Factory.php b/src/Contracts/Factory.php
new file mode 100644
index 0000000..1c2e29d
--- /dev/null
+++ b/src/Contracts/Factory.php
@@ -0,0 +1,16 @@
+fake($result));
return $fake;
}
@@ -43,6 +44,6 @@ public static function fake(ImageResult|PdfResult|null $result = null): Screensh
*/
protected static function getFacadeAccessor(): string
{
- return Factory::class;
+ return ScreenshotManager::class;
}
}
diff --git a/src/ScreenshotManager.php b/src/ScreenshotManager.php
new file mode 100644
index 0000000..8414346
--- /dev/null
+++ b/src/ScreenshotManager.php
@@ -0,0 +1,106 @@
+createScreenshot(new ScreenshotFake),
+ $result,
+ ));
+ }
+
+ /**
+ * Get the default driver name.
+ */
+ public function getDefaultDriver(): string
+ {
+ return $this->config->get('screenshot.default');
+ }
+
+ /**
+ * Get a screenshot factory for the given driver.
+ */
+ public function driver($driver = null): Screenshot
+ {
+ return parent::driver($driver);
+ }
+
+ /**
+ * Wrap the resolved capture driver in a configured screenshot factory.
+ */
+ protected function createDriver($driver): Screenshot
+ {
+ return $this->createScreenshot(parent::createDriver($driver));
+ }
+
+ /**
+ * Create the Chrome capture driver.
+ */
+ protected function createChromeDriver(): ChromeDriver
+ {
+ return new ChromeDriver(
+ $this->config->get('screenshot.browser.binary') ?? $this->container->make(ChromeInstaller::class)->path(),
+ $this->config->get('screenshot.browser.options', []),
+ $this->config->get('screenshot.sockets.directory'),
+ $this->config->get('screenshot.sockets.lock_timeout', 60000),
+ );
+ }
+
+ /**
+ * Create a screenshot factory with the application's capture defaults.
+ */
+ protected function createScreenshot(Driver $driver): Screenshot
+ {
+ $capture = $this->config->get('screenshot.capture');
+ $image = $this->config->get('screenshot.image');
+ $pdf = $this->config->get('screenshot.pdf');
+
+ [$width, $height] = PaperSize::from($pdf['paper'])->dimensions();
+
+ return new Screenshot(
+ driver: $driver,
+ browser: (new BrowserOptions)
+ ->setSize($capture['width'], $capture['height'])
+ ->setTimeouts($capture['load_timeout'], $capture['browser_timeout'])
+ ->setJavascript($capture['javascript'])
+ ->setFailOnTimeout($capture['fail_on_timeout'])
+ ->setSocket($capture['socket']),
+ image: (new ImageOptions)
+ ->setFormat(OutputFormat::from($image['format']), $image['quality'])
+ ->setMaxSize($image['max_width'], $image['max_height'])
+ ->setFullPage($image['full_page']),
+ pdf: (new PdfOptions)
+ ->setLayout(
+ width: $width,
+ height: $height,
+ landscape: $pdf['landscape'],
+ top: $pdf['margins']['top'],
+ right: $pdf['margins']['right'],
+ bottom: $pdf['margins']['bottom'],
+ left: $pdf['margins']['left'],
+ )
+ ->setBackground($pdf['background'])
+ ->setPreferCssPageSize($pdf['prefer_css_page_size']),
+ views: $this->container->make('view'),
+ );
+ }
+}
diff --git a/src/ScreenshotServiceProvider.php b/src/ScreenshotServiceProvider.php
index cef0137..464fa06 100644
--- a/src/ScreenshotServiceProvider.php
+++ b/src/ScreenshotServiceProvider.php
@@ -3,10 +3,7 @@
namespace DirectoryTree\Screenshot;
use DirectoryTree\Screenshot\Commands\InstallCommand;
-use DirectoryTree\Screenshot\Contracts\Driver;
-use DirectoryTree\Screenshot\Drivers\ChromeDriver;
-use DirectoryTree\Screenshot\Enums\OutputFormat;
-use DirectoryTree\Screenshot\Enums\PaperSize;
+use DirectoryTree\Screenshot\Contracts\Factory;
use DirectoryTree\Screenshot\Support\ChromeInstaller;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Support\ServiceProvider;
@@ -24,49 +21,8 @@ public function register(): void
$app['config']->get('screenshot.browser.directory'),
));
- $this->app->singleton(Driver::class, function (Application $app): Driver {
- return new ChromeDriver(
- $app['config']->get('screenshot.browser.binary') ?? $app->make(ChromeInstaller::class)->path(),
- $app['config']->get('screenshot.browser.options', []),
- $app['config']->get('screenshot.sockets.directory'),
- $app['config']->get('screenshot.sockets.lock_timeout', 60000),
- );
- });
-
- $this->app->singleton(Screenshot::class, function (Application $app): Screenshot {
- $capture = $app['config']->get('screenshot.capture');
- $image = $app['config']->get('screenshot.image');
- $pdf = $app['config']->get('screenshot.pdf');
-
- [$width, $height] = PaperSize::from($pdf['paper'])->dimensions();
-
- return new Screenshot(
- driver: $app->make(Driver::class),
- browser: (new BrowserOptions)
- ->setSize($capture['width'], $capture['height'])
- ->setTimeouts($capture['load_timeout'], $capture['browser_timeout'])
- ->setJavascript($capture['javascript'])
- ->setFailOnTimeout($capture['fail_on_timeout'])
- ->setSocket($capture['socket']),
- image: (new ImageOptions)
- ->setFormat(OutputFormat::from($image['format']), $image['quality'])
- ->setMaxSize($image['max_width'], $image['max_height'])
- ->setFullPage($image['full_page']),
- pdf: (new PdfOptions)
- ->setLayout(
- width: $width,
- height: $height,
- landscape: $pdf['landscape'],
- top: $pdf['margins']['top'],
- right: $pdf['margins']['right'],
- bottom: $pdf['margins']['bottom'],
- left: $pdf['margins']['left'],
- )
- ->setBackground($pdf['background'])
- ->setPreferCssPageSize($pdf['prefer_css_page_size']),
- views: $app['view'],
- );
- });
+ $this->app->singleton(ScreenshotManager::class);
+ $this->app->alias(ScreenshotManager::class, Factory::class);
}
/**
diff --git a/src/Testing/ScreenshotManagerFake.php b/src/Testing/ScreenshotManagerFake.php
new file mode 100644
index 0000000..009eaa4
--- /dev/null
+++ b/src/Testing/ScreenshotManagerFake.php
@@ -0,0 +1,48 @@
+fake;
+ }
+
+ /**
+ * Create a fresh recording manager using the same capture defaults.
+ */
+ public function fake(ImageResult|PdfResult|null $result = null): static
+ {
+ return new static(ScreenshotFake::from($this->fake, $result));
+ }
+
+ /**
+ * Forward capture methods and assertions to the shared recorder.
+ */
+ public function __call(string $method, array $parameters): mixed
+ {
+ return $this->forwardCallTo($this->fake, $method, $parameters);
+ }
+}
diff --git a/tests/Browser/ManagerTest.php b/tests/Browser/ManagerTest.php
new file mode 100644
index 0000000..cf5a426
--- /dev/null
+++ b/tests/Browser/ManagerTest.php
@@ -0,0 +1,24 @@
+group('browser');
+
+test('the manager captures images and PDFs through the configured Chrome driver', function () {
+ config([
+ 'screenshot.browser.binary' => getenv('CHROME_PATH') ?: null,
+ 'screenshot.browser.options' => ['noSandbox' => getenv('SCREENSHOT_NO_SANDBOX') === '1'],
+ 'screenshot.capture.width' => 640,
+ 'screenshot.capture.height' => 480,
+ 'screenshot.image.full_page' => false,
+ ]);
+
+ $image = Screenshot::html('Default driver
')->capture();
+ $pdf = Screenshot::driver('chrome')->view('card', ['name' => 'Steve'])->pdf()->capture();
+
+ expect($image->width())->toBe(640);
+ expect($image->height())->toBe(480);
+ expect($image->mimeType())->toBe('image/png');
+ expect($pdf->contents())->toStartWith('%PDF-');
+});
diff --git a/tests/Feature/FactoryTest.php b/tests/Feature/FactoryTest.php
new file mode 100644
index 0000000..ac2d868
--- /dev/null
+++ b/tests/Feature/FactoryTest.php
@@ -0,0 +1,53 @@
+ 'remote']);
+ $driver = new ScreenshotFake;
+ Screenshot::extend('remote', fn () => $driver);
+
+ expect(app(Factory::class))->toBe(app(ScreenshotManager::class));
+
+ app(ScreenshotFactoryConsumer::class)->capture();
+
+ $driver->assertCaptured(fn ($source) => $source === 'invoice');
+ $driver->assertCapturedCount(1);
+});
+
+test('faking replaces a previously resolved factory contract for constructor injection', function ($driver) {
+ config(['screenshot.default' => 'remote']);
+ Screenshot::extend('remote', fn () => throw new RuntimeException('Real driver resolved.'));
+ $original = app(Factory::class);
+ $result = new ImageResult(imageFixture());
+ $fake = Screenshot::fake($result);
+
+ expect(app(Factory::class))->toBe($fake)->not->toBe($original);
+ expect(app(ScreenshotFactoryConsumer::class)->capture($driver))->toBe($result);
+ Screenshot::assertCapturedCount(1);
+
+ $nextResult = new ImageResult(imageFixture());
+ $nextFake = Screenshot::fake($nextResult);
+
+ expect(app(Factory::class))->toBe($nextFake)->not->toBe($fake);
+ Screenshot::assertNothingCaptured();
+ expect(app(ScreenshotFactoryConsumer::class)->capture($driver))->toBe($nextResult);
+ Screenshot::assertCapturedCount(1);
+ $fake->assertCapturedCount(1);
+})->with(['default driver' => null, 'named driver' => 'remote']);
+
+class ScreenshotFactoryConsumer
+{
+ public function __construct(
+ protected Factory $screenshots,
+ ) {}
+
+ public function capture(?string $driver = null): ImageResult
+ {
+ return $this->screenshots->driver($driver)->html('invoice')->capture();
+ }
+}
diff --git a/tests/Feature/InstallCommandTest.php b/tests/Feature/InstallCommandTest.php
index 44fcbe8..2036ea4 100644
--- a/tests/Feature/InstallCommandTest.php
+++ b/tests/Feature/InstallCommandTest.php
@@ -1,6 +1,6 @@
andReturn('/installed/chrome');
app()->instance(ChromeInstaller::class, $installer);
- $driver = app()->make(Driver::class);
+ $factory = Screenshot::driver('chrome');
+ $driver = (new ReflectionProperty($factory, 'driver'))->getValue($factory);
expect((new ReflectionProperty($driver, 'binary'))->getValue($driver))->toBe($expected);
})->with([
diff --git a/tests/Feature/ScreenshotManagerTest.php b/tests/Feature/ScreenshotManagerTest.php
new file mode 100644
index 0000000..88fbd2a
--- /dev/null
+++ b/tests/Feature/ScreenshotManagerTest.php
@@ -0,0 +1,140 @@
+shouldReceive('path')->once()->andReturnNull();
+ app()->instance(ChromeInstaller::class, $installer);
+
+ $manager = app(ScreenshotManager::class);
+
+ expect($manager->getDrivers())->toBe([]);
+ expect($manager->getDefaultDriver())->toBe('chrome');
+ expect($manager->driver())->toBe($manager->driver('chrome'));
+ expect($manager->getDrivers())->toHaveKey('chrome');
+});
+
+test('custom drivers receive the container and are cached until forgotten', function () {
+ $created = 0;
+ $container = app();
+
+ Screenshot::extend('remote', function ($app) use ($container, &$created) {
+ expect($app)->toBe($container);
+ $created++;
+
+ return new ScreenshotFake;
+ });
+
+ expect($created)->toBe(0);
+
+ $factory = Screenshot::driver('remote');
+
+ expect(Screenshot::driver('remote'))->toBe($factory);
+ expect($created)->toBe(1);
+
+ app(ScreenshotManager::class)->forgetDrivers();
+
+ expect(Screenshot::driver('remote'))->not->toBe($factory);
+ expect($created)->toBe(2);
+});
+
+test('default and explicitly selected drivers share the fluent API with independent captures', function () {
+ config(['screenshot.default' => 'remote', 'screenshot.capture.width' => 900]);
+ $installer = Mockery::mock(ChromeInstaller::class);
+ $installer->shouldNotReceive('path');
+ app()->instance(ChromeInstaller::class, $installer);
+ $remote = new ScreenshotFake;
+ $other = new ScreenshotFake;
+
+ Screenshot::extend('remote', fn () => $remote);
+ Screenshot::extend('other', fn () => $other);
+
+ Screenshot::html('first')->size(400, 300)->jpeg()->capture();
+ Screenshot::driver('other')->url('https://example.com/invoice')->element('#invoice')->pdf()->capture();
+ Screenshot::view('card', ['name' => ''])->capture();
+
+ $remote->assertCapturedCount(2);
+ $remote->assertCaptured(fn ($source, $type, $options) => $source === 'first'
+ && $options->format() === OutputFormat::Jpeg
+ && $options->browser()->getWidth() === 400);
+ $remote->assertCaptured(fn ($source, $type, $options) => str_contains($source, '<Steve>')
+ && $type === SourceType::Html
+ && $options->format() === OutputFormat::Png
+ && $options->browser()->getWidth() === 900);
+ $other->assertCapturedCount(1);
+ $other->assertCaptured(fn ($source, $type, $options) => $source === 'https://example.com/invoice'
+ && $type === SourceType::Url
+ && $options->format() === OutputFormat::Pdf
+ && $options->element() === '#invoice'
+ && $options->browser()->getWidth() === 900);
+});
+
+test('custom creators may replace the built-in Chrome driver', function () {
+ $driver = new ScreenshotFake;
+ Screenshot::extend('chrome', fn () => $driver);
+
+ Screenshot::html('custom chrome')->capture();
+
+ $driver->assertCapturedCount(1);
+});
+
+test('unsupported driver names fail on resolution', function () {
+ expect(fn () => Screenshot::driver('missing'))
+ ->toThrow(InvalidArgumentException::class, 'Driver [missing] not supported.');
+});
+
+test('browser shutdowns are forwarded to the selected driver', function () {
+ $driver = Mockery::mock(Driver::class, ManagesBrowsers::class);
+ $driver->shouldReceive('close')->once()->with('invoices');
+ Screenshot::extend('remote', fn () => $driver);
+
+ Screenshot::driver('remote')->close('invoices');
+});
+
+test('the facade recognizes its recording fake', function () {
+ expect(Screenshot::isFake())->toBeFalse();
+
+ Screenshot::fake();
+
+ expect(Screenshot::isFake())->toBeTrue();
+
+ Screenshot::fake();
+
+ expect(Screenshot::isFake())->toBeTrue();
+});
+
+test('the facade fake records captures and shutdowns across every named driver', function () {
+ config(['screenshot.default' => 'remote', 'screenshot.capture.width' => 900]);
+ Screenshot::extend('remote', fn () => throw new RuntimeException('Real driver resolved.'));
+ Screenshot::driver('chrome');
+ $result = new ImageResult(imageFixture());
+ $fake = Screenshot::fake($result);
+
+ expect(app(ScreenshotManager::class))->toBe($fake);
+ expect(Screenshot::html('default')->capture())->toBe($result);
+ expect(Screenshot::driver('chrome')->html('chrome')->capture())->toBe($result);
+ app(ScreenshotManager::class)->driver('other')->view('card', ['name' => 'Steve'])->pdf()->capture();
+ Screenshot::driver('remote')->close('invoices');
+
+ $fake->assertCapturedCount(3);
+ Screenshot::assertCaptured(fn ($source, $type, $options) => $options->format() === OutputFormat::Pdf
+ && $options->browser()->getWidth() === 900);
+ Screenshot::assertClosed('invoices');
+
+ Screenshot::fake();
+
+ Screenshot::assertNothingCaptured();
+ Screenshot::assertNothingClosed();
+ Screenshot::driver('chrome')->html('fresh')->capture();
+ Screenshot::assertCapturedCount(1);
+ $fake->assertCapturedCount(3);
+});
diff --git a/tests/Feature/ScreenshotTest.php b/tests/Feature/ScreenshotTest.php
index de7e20b..c9a2bfc 100644
--- a/tests/Feature/ScreenshotTest.php
+++ b/tests/Feature/ScreenshotTest.php
@@ -9,7 +9,7 @@
use DirectoryTree\Screenshot\ImageResult;
use DirectoryTree\Screenshot\PdfResult;
use DirectoryTree\Screenshot\PendingSource;
-use DirectoryTree\Screenshot\Screenshot as Factory;
+use DirectoryTree\Screenshot\ScreenshotManager;
use DirectoryTree\Screenshot\ScreenshotServiceProvider;
use DirectoryTree\Screenshot\Support\BrowserSocket;
use Illuminate\Filesystem\Filesystem;
@@ -24,20 +24,21 @@
->withArgs(fn ($source, $type, $options) => $source === 'hello' && $options->browser()->getWidth() === 900)
->andReturn(new ImageResult(imageFixture()));
- app()->instance(Driver::class, $driver);
+ Screenshot::extend('custom', fn () => $driver);
+ config(['screenshot.default' => 'custom']);
config(['screenshot.capture.width' => 900, 'screenshot.capture.height' => 600]);
expect(Screenshot::html('hello')->capture()->width())->toBe(1);
});
-test('the facade fake replaces the container factory and renders escaped Blade views', function () {
+test('the facade fake replaces the container manager and renders escaped Blade views', function () {
config(['screenshot.image.quality' => 85]);
Screenshot::fake();
Screenshot::assertNothingCaptured();
- app()->make(Factory::class)
+ app()->make(ScreenshotManager::class)
->view('card', ['name' => ''])
->jpeg()
->capture();
@@ -400,10 +401,10 @@
);
});
-test('invalid configured defaults fail when the factory is resolved', function ($key, $value) {
+test('invalid configured defaults fail when a driver is resolved', function ($key, $value) {
config([$key => $value]);
- expect(fn () => app(Factory::class))->toThrow(InvalidArgumentException::class);
+ expect(fn () => app(ScreenshotManager::class)->driver())->toThrow(InvalidArgumentException::class);
})->with([
'browser dimensions' => ['screenshot.capture.width', 0],
'browser timeout' => ['screenshot.capture.browser_timeout', 0],
diff --git a/tests/Unit/ScreenshotManagerFakeTest.php b/tests/Unit/ScreenshotManagerFakeTest.php
new file mode 100644
index 0000000..f4dcc8e
--- /dev/null
+++ b/tests/Unit/ScreenshotManagerFakeTest.php
@@ -0,0 +1,31 @@
+setSize(900, 600),
+ ));
+
+ $fake->html('default')->capture();
+ $fake->driver('remote')->html('remote')->size(320, 240)->capture();
+ $fake->close(key: 'invoices');
+
+ $fake->assertCapturedCount(count: 2);
+ $fake->assertClosed(key: 'invoices');
+
+ $result = new ImageResult(imageFixture());
+ $fresh = $fake->fake($result);
+
+ $fresh->assertNothingCaptured();
+ $fresh->assertNothingClosed();
+ expect($fresh->driver('chrome')->html('fresh')->capture())->toBe($result);
+ $fresh->assertCaptured(fn ($source, $type, $options) => $source === 'fresh'
+ && $options->browser()->getWidth() === 900
+ && $options->browser()->getHeight() === 600);
+ $fresh->assertCapturedCount(1);
+ $fake->assertCapturedCount(2);
+});