├── .editorconfig ├── .gitattributes ├── .gitignore ├── README.md ├── application ├── bootstrap └── app.php ├── composer.json ├── composer.lock ├── config ├── app.php ├── commands.php └── laravel-console-dusk.php ├── phpunit.dusk.xml ├── storage ├── log │ └── .gitignore ├── screenshots │ └── .gitignore └── source │ └── .gitignore └── tests ├── Chrome.php ├── CreatesApplication.php ├── CreatesBrowser.php ├── DuskTestCase.php ├── ExampleTest.php ├── Pages └── ExamplePage.php └── Pest.php /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 4 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [*.yml] 15 | indent_style = space 16 | indent_size = 2 17 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | /.github export-ignore 3 | .styleci.yml export-ignore 4 | .scrutinizer.yml export-ignore 5 | BACKERS.md export-ignore 6 | CONTRIBUTING.md export-ignore 7 | CHANGELOG.md export-ignore 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | .phpunit.result.cache 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel Husk 2 | 3 | Laravel Husk is a thin and light scaffolded Laravel Dusk environment. 4 | 5 | It allows you to test your JavaScript applications with PHP using [Pest](https://pestphp.com), without having to scaffold an entire Laravel application. 6 | 7 | ## Examples 8 | 9 | | JS Framework | Tests | 10 | | ---------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | 11 | | [NuxtJS](https://github.com/stevebauman/laravel-husk-nuxt) | ![Nuxt Tests](https://github.com/stevebauman/laravel-husk-nuxt/actions/workflows/run-tests.yml/badge.svg) | 12 | | [NextJS](https://github.com/stevebauman/laravel-husk-next) | ![Next Tests](https://github.com/stevebauman/laravel-husk-next/actions/workflows/run-tests.yml/badge.svg) | 13 | | [Svelte](https://github.com/stevebauman/laravel-husk-svelte) | ![Svelte Tests](https://github.com/stevebauman/laravel-husk-svelte/actions/workflows/run-tests.yml/badge.svg) | 14 | | [Gatsby](https://github.com/stevebauman/laravel-husk-gatsby) | ![Gatsby Tests](https://github.com/stevebauman/laravel-husk-gatsby/actions/workflows/run-tests.yml/badge.svg) | 15 | | [Gridsome](https://github.com/stevebauman/laravel-husk-gridsome) | ![Gridsome Tests](https://github.com/stevebauman/laravel-husk-gridsome/actions/workflows/run-tests.yml/badge.svg) | 16 | | [Showcode (NuxtJS)](https://github.com/stevebauman/showcode) | ![Gridsome Tests](https://github.com/stevebauman/showcode/actions/workflows/run-tests.yml/badge.svg) | 17 | 18 | ## Installation 19 | 20 | Inside of your JavaScript application folder, run the below command to scaffold the Laravel Husk environment: 21 | 22 | > **Note**: This will create the folder named `browser` which will contain your Laravel Husk test environment. 23 | 24 | ```bash 25 | composer create-project stevebauman/laravel-husk browser 26 | ``` 27 | 28 | After scaffolding the test environment, you should have the below folder structure; 29 | 30 | ``` 31 | javascript-app/ 32 | ├── ... 33 | └── browser/ 34 | ├── bootstrap/ 35 | ├── config/ 36 | ├── storage/ 37 | │ ├── log/ 38 | │ ├── screenshots/ 39 | │ └── source/ 40 | └── tests/ 41 | ├── ... 42 | ├── ExampleTest.php 43 | ├── DuskTestCase.php 44 | └── Pages/ 45 | └── ExamplePage.php 46 | ``` 47 | 48 | Then, navigate into the `browser` directory and install the Chrome driver by running the below command: 49 | 50 | ``` 51 | php application dusk:chrome-driver --detect 52 | ``` 53 | 54 | ## Usage 55 | 56 | Before running your dusk tests, be sure to set the proper base URL to where your JavaScript application will be served from: 57 | 58 | ```php 59 | // tests/DuskTestCase.php 60 | 61 | protected function setUp(): void 62 | { 63 | parent::setUp(); 64 | 65 | $this->setupBrowser('http://localhost:3000'); 66 | } 67 | ``` 68 | 69 | After setting the base URL, serve your JavaScript application: 70 | 71 | ```bash 72 | npm run dev 73 | ``` 74 | 75 | Then, inside of another terminal, navigate into the `browser` directory: 76 | 77 | ```bash 78 | cd browser 79 | ``` 80 | 81 | And run the below command: 82 | 83 | > **Important**: Make sure you've installed the Chrome driver first, via `php application dusk:chrome-driver --detect` 84 | 85 | ```bash 86 | php application pest:dusk 87 | ``` 88 | 89 | With arguments: 90 | 91 | ```bash 92 | php application pest:dusk --order-by=random --filter="it can load" 93 | ``` 94 | 95 | > **Note**: You may also insert the below JSON into the `scripts` section of your `package.json` file to run your browser tests from your root project directory: 96 | > 97 | > ```json 98 | > "scripts": { 99 | > "test": "cd browser && php application pest:dusk" 100 | > } 101 | > ``` 102 | > 103 | > ```bash 104 | > npm run test 105 | > ``` 106 | 107 | ## GitHub Actions 108 | 109 | You may use the below GitHub action as a template to run your Laravel Dusk tests: 110 | 111 | ```yaml 112 | name: run-tests 113 | 114 | on: 115 | push: 116 | pull_request: 117 | schedule: 118 | - cron: "0 0 * * *" 119 | 120 | jobs: 121 | run-tests: 122 | runs-on: ubuntu-latest 123 | steps: 124 | - uses: actions/checkout@v2 125 | - uses: actions/setup-node@v2 126 | with: 127 | cache: "npm" 128 | 129 | - name: Install Javascript Dependencies 130 | run: npm install 131 | 132 | - name: Start JavaScript Application 133 | run: npm run dev & 134 | 135 | - name: Install Composer Dependencies 136 | working-directory: ./browser 137 | run: composer install --no-progress --prefer-dist --optimize-autoloader 138 | 139 | - name: Upgrade Chrome Driver 140 | working-directory: ./browser 141 | run: php application dusk:chrome-driver `/opt/google/chrome/chrome --version | cut -d " " -f3 | cut -d "." -f1` 142 | 143 | - name: Run Dusk Tests 144 | working-directory: ./browser 145 | run: php application pest:dusk 146 | 147 | - name: Upload Screenshots 148 | if: failure() 149 | uses: actions/upload-artifact@v2 150 | with: 151 | name: screenshots 152 | path: browser/storage/screenshots 153 | 154 | - name: Upload Console Logs 155 | if: failure() 156 | uses: actions/upload-artifact@v2 157 | with: 158 | name: console 159 | path: browser/storage/console 160 | ``` 161 | -------------------------------------------------------------------------------- /application: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | make(Illuminate\Contracts\Console\Kernel::class); 34 | 35 | $status = $kernel->handle( 36 | $input = new Symfony\Component\Console\Input\ArgvInput, 37 | new Symfony\Component\Console\Output\ConsoleOutput 38 | ); 39 | 40 | /* 41 | |-------------------------------------------------------------------------- 42 | | Shutdown The Application 43 | |-------------------------------------------------------------------------- 44 | | 45 | | Once Artisan has finished running, we will fire off the shutdown events 46 | | so that any final work may be done by the application before we shut 47 | | down the process. This is the last thing to happen to the request. 48 | | 49 | */ 50 | 51 | $kernel->terminate($input, $status); 52 | 53 | exit($status); 54 | -------------------------------------------------------------------------------- /bootstrap/app.php: -------------------------------------------------------------------------------- 1 | singleton( 30 | Illuminate\Contracts\Console\Kernel::class, 31 | LaravelZero\Framework\Kernel::class 32 | ); 33 | 34 | $app->singleton( 35 | Illuminate\Contracts\Debug\ExceptionHandler::class, 36 | Illuminate\Foundation\Exceptions\Handler::class 37 | ); 38 | 39 | /* 40 | |-------------------------------------------------------------------------- 41 | | Return The Application 42 | |-------------------------------------------------------------------------- 43 | | 44 | | This script returns the application instance. The instance is given to 45 | | the calling script so we can separate the building of the instances 46 | | from the actual running of the application and sending responses. 47 | | 48 | */ 49 | 50 | return $app; 51 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stevebauman/laravel-husk", 3 | "description": "A Laravel Dusk environment for your Javascript only applications.", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "Steve Bauman", 8 | "email": "steven_bauman@outlook.com" 9 | } 10 | ], 11 | "autoload-dev": { 12 | "psr-4": { 13 | "Tests\\": "tests/" 14 | } 15 | }, 16 | "require": { 17 | "php": "^8.0", 18 | "laravel-zero/framework": "^10.0", 19 | "nunomaduro/laravel-console-dusk": "^1.11", 20 | "nunomaduro/termwind": "^1.15" 21 | }, 22 | "require-dev": { 23 | "fakerphp/faker": "^1.23", 24 | "laravel/dusk": "^7.9", 25 | "mockery/mockery": "^1.6.6", 26 | "pestphp/pest": "^2.16" 27 | }, 28 | "config": { 29 | "allow-plugins": { 30 | "pestphp/pest-plugin": true 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /config/app.php: -------------------------------------------------------------------------------- 1 | 'Application', 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Application Version 21 | |-------------------------------------------------------------------------- 22 | | 23 | | This value determines the "version" your application is currently running 24 | | in. You may want to follow the "Semantic Versioning" - Given a version 25 | | number MAJOR.MINOR.PATCH when an update happens: https://semver.org. 26 | | 27 | */ 28 | 29 | 'version' => app('git.version'), 30 | 31 | /* 32 | |-------------------------------------------------------------------------- 33 | | Application Environment 34 | |-------------------------------------------------------------------------- 35 | | 36 | | This value determines the "environment" your application is currently 37 | | running in. This may determine how you prefer to configure various 38 | | services the application utilizes. This can be overridden using 39 | | the global command line "--env" option when calling commands. 40 | | 41 | */ 42 | 43 | 'env' => 'development', 44 | 45 | /* 46 | |-------------------------------------------------------------------------- 47 | | Autoloaded Service Providers 48 | |-------------------------------------------------------------------------- 49 | | 50 | | The service providers listed here will be automatically loaded on the 51 | | request to your application. Feel free to add your own services to 52 | | this array to grant expanded functionality to your applications. 53 | | 54 | */ 55 | 56 | 'providers' => [], 57 | 58 | ]; 59 | -------------------------------------------------------------------------------- /config/commands.php: -------------------------------------------------------------------------------- 1 | NunoMaduro\LaravelConsoleSummary\SummaryCommand::class, 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Commands Paths 21 | |-------------------------------------------------------------------------- 22 | | 23 | | This value determines the "paths" that should be loaded by the console's 24 | | kernel. Foreach "path" present on the array provided below the kernel 25 | | will extract all "Illuminate\Console\Command" based class commands. 26 | | 27 | */ 28 | 29 | 'paths' => [], 30 | 31 | /* 32 | |-------------------------------------------------------------------------- 33 | | Added Commands 34 | |-------------------------------------------------------------------------- 35 | | 36 | | You may want to include a single command class without having to load an 37 | | entire folder. Here you can specify which commands should be added to 38 | | your list of commands. The console's kernel will try to load them. 39 | | 40 | */ 41 | 42 | 'add' => [], 43 | 44 | /* 45 | |-------------------------------------------------------------------------- 46 | | Hidden Commands 47 | |-------------------------------------------------------------------------- 48 | | 49 | | Your application commands will always be visible on the application list 50 | | of commands. But you can still make them "hidden" specifying an array 51 | | of commands below. All "hidden" commands can still be run/executed. 52 | | 53 | */ 54 | 55 | 'hidden' => [ 56 | NunoMaduro\LaravelConsoleSummary\SummaryCommand::class, 57 | Symfony\Component\Console\Command\DumpCompletionCommand::class, 58 | Symfony\Component\Console\Command\HelpCommand::class, 59 | Illuminate\Console\Scheduling\ScheduleRunCommand::class, 60 | Illuminate\Console\Scheduling\ScheduleListCommand::class, 61 | Illuminate\Console\Scheduling\ScheduleFinishCommand::class, 62 | LaravelZero\Framework\Commands\StubPublishCommand::class, 63 | ], 64 | 65 | /* 66 | |-------------------------------------------------------------------------- 67 | | Removed Commands 68 | |-------------------------------------------------------------------------- 69 | | 70 | | Do you have a service provider that loads a list of commands that 71 | | you don't need? No problem. Laravel Zero allows you to specify 72 | | below a list of commands that you don't to see in your app. 73 | | 74 | */ 75 | 76 | 'remove' => [ 77 | // .. 78 | ], 79 | 80 | ]; 81 | -------------------------------------------------------------------------------- /config/laravel-console-dusk.php: -------------------------------------------------------------------------------- 1 | [ 14 | 'screenshots' => storage_path('screenshots'), 15 | 'source' => storage_path('source'), 16 | 'log' => storage_path('log'), 17 | ], 18 | 19 | /* 20 | | -------------------------------------------------------------------------- 21 | | Headless Mode 22 | | -------------------------------------------------------------------------- 23 | | 24 | | When false it will show a Chrome window while running. Within production 25 | | it will be forced to run in headless mode. 26 | */ 27 | 28 | 'headless' => true, 29 | 30 | ]; 31 | -------------------------------------------------------------------------------- /phpunit.dusk.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | ./tests 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /storage/log/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/screenshots/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/source/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /tests/Chrome.php: -------------------------------------------------------------------------------- 1 | close(); 23 | } 24 | 25 | /** 26 | * Start the Chrome driver. 27 | * 28 | * @return void 29 | */ 30 | public function open(): void 31 | { 32 | static::startChromeDriver(); 33 | } 34 | 35 | /** 36 | * Close the Chrome driver. 37 | * 38 | * @return void 39 | */ 40 | public function close(): void 41 | { 42 | static::stopChromeDriver(); 43 | } 44 | 45 | /** 46 | * Purposely left empty. 47 | * 48 | * @param Closure $callback 49 | * 50 | * @return void 51 | */ 52 | public static function afterClass(Closure $callback) 53 | { 54 | // .. 55 | } 56 | 57 | /** 58 | * Get the web driver instance. 59 | * 60 | * @return RemoteWebDriver 61 | */ 62 | public function getDriver() 63 | { 64 | $options = (new ChromeOptions)->addArguments([ 65 | '--window-size=1920,1080', 66 | '--disable-gpu', 67 | $this->runHeadless(), 68 | ]); 69 | 70 | return RemoteWebDriver::create( 71 | 'http://localhost:9515', 72 | DesiredCapabilities::chrome()->setCapability( 73 | ChromeOptions::CAPABILITY, 74 | $options 75 | ) 76 | ); 77 | } 78 | 79 | /** 80 | * Whether to run in headless mode. 81 | * 82 | * @return string|null 83 | */ 84 | protected function runHeadless() 85 | { 86 | return config('laravel-console-dusk.headless', true) ? '--headless' : null; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /tests/CreatesApplication.php: -------------------------------------------------------------------------------- 1 | make(Kernel::class)->bootstrap(); 19 | 20 | return $app; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/CreatesBrowser.php: -------------------------------------------------------------------------------- 1 | driver = new Chrome($options); 27 | 28 | Browser::$baseUrl = $url; 29 | 30 | Browser::$storeScreenshotsAt = config('laravel-console-dusk.paths.screenshots'); 31 | Browser::$storeConsoleLogAt = config('laravel-console-dusk.paths.log'); 32 | Browser::$storeSourceAt = config('laravel-console-dusk.paths.source'); 33 | 34 | $this->driver->open(); 35 | } 36 | 37 | /** 38 | * Get the Laravel Dusk driver. 39 | * 40 | * @return Chrome 41 | */ 42 | protected function driver() 43 | { 44 | return $this->driver->getDriver(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/DuskTestCase.php: -------------------------------------------------------------------------------- 1 | setupBrowser('http://localhost:3000'); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/ExampleTest.php: -------------------------------------------------------------------------------- 1 | browse(function (Browser $browser) { 8 | $browser->visit(new ExamplePage)->assertSee('Hello World'); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /tests/Pages/ExamplePage.php: -------------------------------------------------------------------------------- 1 | waitUntilMissing('#nuxt-loading')->waitFor('#__layout'); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/Pest.php: -------------------------------------------------------------------------------- 1 | in(__DIR__); 15 | 16 | /* 17 | |-------------------------------------------------------------------------- 18 | | Expectations 19 | |-------------------------------------------------------------------------- 20 | | 21 | | When you're writing tests, you often need to check that values meet certain conditions. The 22 | | "expect()" function gives you access to a set of "expectations" methods that you can use 23 | | to assert different things. Of course, you may extend the Expectation API at any time. 24 | | 25 | */ 26 | 27 | expect()->extend('toBeOne', function () { 28 | return $this->toBe(1); 29 | }); 30 | 31 | /* 32 | |-------------------------------------------------------------------------- 33 | | Functions 34 | |-------------------------------------------------------------------------- 35 | | 36 | | While Pest is very powerful out-of-the-box, you may have some testing code specific to your 37 | | project that you don't want to repeat in every file. Here you can also expose helpers as 38 | | global functions to help you to reduce the number of lines of code in your test files. 39 | | 40 | */ 41 | 42 | // function something() 43 | // { 44 | // 45 | // } 46 | --------------------------------------------------------------------------------