Search code examples
laravellaravel-dusk

Pressing button in Laravel Dusk test results in blank screen on GitHub Actions


I've written the following end-to-end test with Laravel Dusk to test my site's PayPal and Stripe Element forms:

<?php

namespace Tests\Browser;

use Illuminate\Foundation\Testing\DatabaseMigrations;
use Laravel\Dusk\Browser;
use Tests\DuskTestCase;

class PaymentsTest extends DuskTestCase
{
    use DatabaseMigrations;

    /**
    * A Dusk test example.
    * 
    * @return void
    */
    public function test_payments_are_working()
    {
        $this->browse(function (Browser $browser) {
            $browser->visit("/manufacturers/oneplus")
            ->type('email', "[email protected]")
            ->press('Continue')
            ->waitForLocation('/payment')
            ->waitFor("#paypal-button-container iframe")
            ->withinFrame('#paypal-button-container iframe', function($browser) {
                $browser->click(".paypal-button");
            })
            ->waitFor(".paypal-checkout-sandbox-iframe")
            ->withinFrame('.paypal-checkout-sandbox-iframe', function($browser) {
                $browser->click(".paypal-checkout-close");
            })
            ->waitFor('.__PrivateStripeElement iframe')
            ->withinFrame('.__PrivateStripeElement iframe', function($browser) {
                $browser
                ->pause(1000)
                ->keys('input[placeholder="1234 1234 1234 1234"]', 4242424242424242)
                ->keys('input[placeholder="MM / YY"]', '0125')
                ->keys('input[placeholder="CVC"]', '123')
                ->select('select[name="country"]', 'GB')
                ->keys('input[name="postalCode"]', 'SW1A 1AA')
                ->pause(500);
            })
            ->clickAndWaitForReload("@stripe-payment-button")
            ->pause(10000) // for debugging purposes
            ->assertSeeAnythingIn(".text-success");
        });
    }
}

The entire test works right up until the final assertion:

Time: 00:19.175, Memory: 30.00 MB

There was 1 error:

1) Tests\Browser\PaymentsTest::test_payments_are_working
Facebook\WebDriver\Exception\NoSuchElementException: no such element: Unable to locate element: {"method":"css selector","selector":"body .text-success"}
(Session info: headless chrome=100.0.4896.75)

Looking at the screenshot generated by my GitHub Action workflow at the point of failure, I can see that immediately after the button is pressed it becomes a blank page instead of redirecting to the expected route, even though prior to this all the routes are working.

What could be going on here that would cause this one route to become blank?

What I've Tried So Far:

  • Setting APP_URL in .env.dusk.testing to http://127.0.0.1:8000 and running Dusk with php artisan dusk --env=testing
  • Adding a step to run migrations in my GitHub Action, which was missing from the example in the documentation

Solution

  • After a few days of trying to debug this infuriating issue in order to test my Stripe and PayPal payments - made only a little easier by watching the tests run locally using php artisan dusk --browse - I eventually realised the primary cause of the issue was a HTTPS issue despite having already set my APP_URL to HTTP... although even after solving that issue I still had to fix a few other issues with the Dusk documentation's example GitHub Action just to get the whole setup working.


    I first combed through my Stripe client side code and confirmed that my return_url variable was hardcoded to a HTTPS link on successful payment, which is why that route alone was failing to display. Simply changing this variable to http (neither the Stripe nor PayPal payment success routes accept relative links) would solve the problem, but pushing this file into version control will also cause the live site to redirect the insecure HTTP route, which itself will cause the Stripe live integration to fail. Therefore this requires a solution which avoids hardcoding the payment return URLs into the JavaScript.

    I decided to solve it by using Laravel Mix's ability to dynamically inject variables from my .env file into my JavaScript, as briefly described in the documentation here.

    First, make sure your .env.dusk.testing file contains the following values:

    APP_ENV=testing
    APP_URL=http://127.0.0.1:8000 # Leave this as is
    MIX_APP_URL="${APP_URL}"
    

    Second, make sure your .env.dusk.local file contains the following values:

    APP_ENV=local
    APP_URL=http://localhost # This should be a HTTP link to your local development server
    MIX_APP_URL="${APP_URL}"
    

    Finally, make sure the .env file on your production server has something like the following:

    APP_ENV=production
    APP_URL=https://www.example.com # Make sure this is HTTPS
    MIX_APP_URL="${APP_URL}"
    

    In my client-side JavaScript code, such as for Stripe payments, I can then grab the environment's relevant APP_URL by doing:

    const { error } = await stripe.confirmPayment({
        elements,
        confirmParams: {
            return_url: process.env.MIX_APP_URL + "/success",
        },
    });
    

    ...where /success represents the rest of the path to my payment success route.

    Next, add the standard setup-node action to install a recent version of NPM to your GitHub Action, followed by a build step to run Laravel Mix and replace process.env.MIX_APP_URL your other Mix variables with the appropriate values:

      - uses: actions/setup-node@v3
        with:
          node-version: "14"
    
     ...
    
      - name: Build NPM for production
        run: |
          npm ci
          npm run dev
    

    Lastly, change the environment file preparation stage to copy environment variables from env.dusk.testing (or .env.dusk.local if that's all you have) instead of the default .env.example used by the documentation:

      - name: Prepare The Environment
        run: cp .env.dusk.testing .env
    

    This should result in a complete testing workflow for GitHub Actions that looks similar to the following:

    jobs:
      dusk-php:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v3
          - uses: actions/setup-node@v3
            with:
              node-version: "14"
          - name: Prepare The Environment
            run: cp .env.dusk.testing .env
          - name: Create Database
            run: |
              sudo systemctl start mysql
              mysql --user="root" --password="root" -e "CREATE DATABASE CICD_test_db character set UTF8mb4 collate utf8mb4_bin;"
          - name: Install Composer Dependencies
            run: composer install --no-progress --prefer-dist --optimize-autoloader
          - name: Build NPM for production
            run: |
              npm ci
              npm run prod
          - name: Generate Application Key
            run: php artisan key:generate
          - name: Upgrade Chrome Driver
            run: php artisan dusk:chrome-driver `/opt/google/chrome/chrome --version | cut -d " " -f3 | cut -d "." -f1`
          - name: Run Migrations
            run: php artisan migrate:fresh
          - name: Start Chrome Driver
            run: ./vendor/laravel/dusk/bin/chromedriver-linux &
          - name: Run Laravel Server
            run: php artisan serve --no-reload &
          - name: Run Dusk Tests
            run: php artisan dusk -vvv --env=testing
          - name: Upload Screenshots
            if: failure()
            uses: actions/upload-artifact@v2
            with:
              name: screenshots
              path: tests/Browser/screenshots
          - name: Upload Console Logs
            if: failure()
            uses: actions/upload-artifact@v2
            with:
              name: console
              path: tests/Browser/console
    

    ...and most importantly, successfully runs your Laravel Dusk tests.