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:
APP_URL
in .env.dusk.testing
to http://127.0.0.1:8000
and running Dusk with php artisan dusk --env=testing
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.