Files
DatBrowser/app/Browser/BrowserJob.php
Matthias Guillitte 6a95653c52
All checks were successful
Push image to registry / build-image (push) Successful in 5m20s
Added notification on job fail
2025-03-01 15:12:15 +01:00

292 lines
8.9 KiB
PHP

<?php
namespace App\Browser;
use App\Models\Job;
use App\Models\JobArtifact;
use App\Models\JobRun;
use App\Notification\Notifications\JobErrorNotification;
use App\Notification\Providers\AllNotification;
use Closure;
use Exception;
use Facebook\WebDriver\Chrome\ChromeOptions;
use Facebook\WebDriver\Remote\DesiredCapabilities;
use Facebook\WebDriver\Remote\RemoteWebDriver;
use Facebook\WebDriver\Remote\RemoteWebElement;
use Facebook\WebDriver\WebDriverBy;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Laravel\Dusk\Browser;
use Laravel\Dusk\Chrome\SupportsChrome;
use Laravel\Dusk\Concerns\ProvidesBrowser;
use PHPUnit\Framework\Attributes\BeforeClass;
use Throwable;
abstract class BrowserJob implements ShouldQueue
{
use SupportsChrome, ProvidesBrowser, Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public int $jobId;
public $timeout = 500;
public function __construct(int $jobId)
{
$this->jobId = $jobId;
}
/**
* Execute the callback in a browser
* @param Closure $callback function with a Browser as parameter
* @return void
*/
private function executeInBrowser(Closure $callback): ?JobRun
{
$this->prepare();
$this->setUp();
$this->browse(function (Browser $browser) use ($callback, &$log) {
try {
$log = $callback($browser);
// } catch (Exception $e) {
// $browser->screenshot("failure-{$this->jobId}");
// dump($e);
// throw $e;
}
catch (Throwable $e) {
$browser->screenshot("failure-{$this->jobId}");
AllNotification::send(new JobErrorNotification($this->jobId, $e->getMessage()));
dump($e);
throw $e;
} finally {
$browser->quit();
return [];
}
});
return $log;
}
/**
* Execute the job
* @return void
*/
public function execute(): ?JobRun {
return $this->executeInBrowser(function (Browser $browser): ?JobRun {
return $this->run($browser);
});
}
/**
* Execute the job test
* @return void
*/
public function executeTest(): ?JobRun {
return $this->executeInBrowser(function (Browser $browser): ?JobRun {
return $this->runTest($browser);
});
}
/**
* Steps that run in the browser
* @param \Laravel\Dusk\Browser $browser
* @return void
*/
abstract public function run(Browser $browser): ?JobRun;
abstract public function runTest(Browser $browser): ?JobRun;
/**
* Prepare for Dusk test execution.
* @unused
*/
#[BeforeClass]
public static function prepare(): void
{
if (config("dusk.driver.url") == null && !(isset($_ENV['LARAVEL_SAIL']) && $_ENV['LARAVEL_SAIL'] == '1')) {
static::startChromeDriver(['--port=9515']);
}
}
/**
* Register the base URL with Dusk.
*
* @return void
*/
protected function setUp(): void
{
Browser::$baseUrl = "https://pdftools.matthiasg.dev/";
Browser::$storeScreenshotsAt = base_path('app/Browser/screenshots');
Browser::$storeConsoleLogAt = base_path('app/Browser/console');
Browser::$storeSourceAt = base_path('app/Browser/source');
/*Browser::$userResolver = function () {
return $this->user();
}; */
}
protected function makeSimpleJobRun(bool $success, string $name, string $content): JobRun {
$artifact = new JobRun([
"job_id" => $this->jobId,
"success" => $success
]);
$artifact->save();
$artifact->artifacts()->save(new JobArtifact([
"name" => $name,
"content" => $content,
]));
return $artifact;
}
/**
* Create the RemoteWebDriver instance.
*/
protected function driver(): RemoteWebDriver
{
$options = (new ChromeOptions)->addArguments(collect([
$this->shouldStartMaximized() ? '--start-maximized' : '--window-size=1360,1020',
'--disable-search-engine-choice-screen',
'--disable-gpu',
'--no-sandbox',
'--disable-setuid-sandbox',
'--whitelisted-ips=""',
'--disable-dev-shm-usage',
'--user-data-dir=/home/seluser/profile/',
])->all());
return RemoteWebDriver::create(
config("dusk.driver.url", 'http://localhost:9515'),
DesiredCapabilities::chrome()->setCapability(
ChromeOptions::CAPABILITY,
$options
)
);
}
public function terminate() {
$this->browse(function (Browser $browser) {
$browser->quit();
});
}
/**
* Determine whether the Dusk command has disabled headless mode.
*/
protected function hasHeadlessDisabled(): bool
{
return isset($_SERVER['DUSK_HEADLESS_DISABLED']) ||
isset($_ENV['DUSK_HEADLESS_DISABLED']);
}
/**
* Determine if the browser window should start maximized.
*/
protected function shouldStartMaximized(): bool
{
return isset($_SERVER['DUSK_START_MAXIMIZED']) ||
isset($_ENV['DUSK_START_MAXIMIZED']);
}
/**
* Register an "after class" tear down callback.
*
* @param \Closure $callback
* @return void
*/
public static function afterClass(Closure $callback)
{
static::$afterClassCallbacks[] = $callback;
}
public static function name()
{
return "test";
}
public static function dataName()
{
return "dataTest";
}
/**
* Execute the job.
*/
public function handle(): void
{
if (Job::find($this->jobId)->is_active) {
$this->execute();
}
}
public function reschedule($minutes) {
$this::dispatch()->delay(now()->addMinutes($minutes));
}
// === BROWSER MACROS ===
protected function waitForAndClickText(Browser $browser, string $text, int $timeout = 30, bool $ignoreCase = true) {
$browser->waitForText($text, $timeout, $ignoreCase);
$this->findElementContainingText($browser, $text, $ignoreCase)?->click();
}
protected function waitForElementContainingTextAndGetIt(Browser $browser, string $text, int $timeout = 30, bool $ignoreCase = true): RemoteWebElement|null {
try {
$browser->waitForText($text, $timeout, $ignoreCase);
return $this->findElementContainingText($browser, $text, $ignoreCase);
} catch (Exception $e) {
return null;
}
}
protected function findElementContainingText(Browser $browser, string $text, bool $ignoreCase = true): RemoteWebElement|null {
try {
if ($ignoreCase) {
return $browser->driver->findElement(WebDriverBy::xpath("//*[{$this->xpathContainsIgnoreCase($text)}]"));
} else {
return $browser->driver->findElement(WebDriverBy::xpath("//*[contains(text(), \"{$text}\")]"));
}
} catch (Exception $e) {
return null;
}
}
private function xpathContainsIgnoreCase(string $needle, string $haystack = "text()") {
$needle = strtolower($needle);
return "contains(translate({$haystack}, \"ABCDEFGHIJKLMNOPQRSTUVWXYZ\", \"abcdefghijklmnopqrstuvwxyz\"), \"{$needle}\")";
}
protected function waitForAndClickElementContainingText(Browser $browser, string $elementXpath, string $text, int $timeout = 30, bool $ignoreCase = true) {
$browser->waitForText($text, $timeout, $ignoreCase);
$this->findElementContainingElementWithText($browser, $elementXpath, $text, $ignoreCase)?->click();
}
protected function waitForElementContainingElementWithTextAndGetIt(Browser $browser, string $elementXpath, string $text, int $timeout = 30, bool $ignoreCase = true): RemoteWebElement|null {
try {
$browser->waitForText($text, $timeout, $ignoreCase);
sleep(2);
return $this->findElementContainingElementWithText($browser, $elementXpath, $text, $ignoreCase);
} catch (Exception $e) {
return null;
}
}
protected function findElementContainingElementWithText(Browser $browser, string $elementXpath, string $text, bool $ignoreCase = true): RemoteWebElement|null {
try {
if ($ignoreCase) {
return $browser->driver->findElement(WebDriverBy::xpath("{$elementXpath}[.//*[{$this->xpathContainsIgnoreCase($text)}]]"));
} else {
return $browser->driver->findElement(WebDriverBy::xpath("{$elementXpath}[.//*[contains(text(), \"{$text}\")]]"));
}
} catch (Exception $e) {
return null;
}
}
}