added countries support expanded for ISO 3166 standard merged in new support
This commit is contained in:
@@ -1,25 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Support\Facades\Route;
|
|
||||||
use Modules\Countries\Http\Controllers\Api\CountriesApiController;
|
|
||||||
use Modules\Countries\Http\Controllers\Api\ReferenceDataApiController;
|
|
||||||
|
|
||||||
Route::prefix('api/v1/countries')->name('api.countries.')->group(function (): void {
|
|
||||||
Route::get('/', [CountriesApiController::class, 'index'])->name('index');
|
|
||||||
Route::get('/active', [CountriesApiController::class, 'active'])->name('active');
|
|
||||||
Route::get('/resolve/{code}', [CountriesApiController::class, 'resolve'])
|
|
||||||
->where('code', '[A-Za-z0-9]{2,3}')
|
|
||||||
->name('resolve');
|
|
||||||
Route::get('/{country:alpha2}', [CountriesApiController::class, 'show'])->name('show');
|
|
||||||
Route::get('/{alpha2}/subdivisions', [CountriesApiController::class, 'subdivisions'])
|
|
||||||
->where('alpha2', '[A-Za-z]{2}')
|
|
||||||
->name('subdivisions');
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
Route::prefix('api/v1')->name('api.reference.')->group(function (): void {
|
|
||||||
Route::get('/currencies', [ReferenceDataApiController::class, 'currencies'])->name('currencies.index');
|
|
||||||
Route::get('/languages', [ReferenceDataApiController::class, 'languages'])->name('languages.index');
|
|
||||||
Route::get('/scripts', [ReferenceDataApiController::class, 'scripts'])->name('scripts.index');
|
|
||||||
Route::get('/locodes', [ReferenceDataApiController::class, 'locodes'])->name('locodes.index');
|
|
||||||
});
|
|
||||||
@@ -14,6 +14,17 @@ class DocumentationModuleTest extends TestCase
|
|||||||
{
|
{
|
||||||
use RefreshDatabase;
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a CSRF token for projects that keep web middleware enabled during tests.
|
||||||
|
*
|
||||||
|
* @param array<string, mixed> $payload
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
private function csrfPayload(array $payload): array
|
||||||
|
{
|
||||||
|
return array_merge(['_token' => 'test-token'], $payload);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The documentation dashboard renders for authenticated users.
|
* The documentation dashboard renders for authenticated users.
|
||||||
*/
|
*/
|
||||||
@@ -40,7 +51,7 @@ class DocumentationModuleTest extends TestCase
|
|||||||
'is_active' => true,
|
'is_active' => true,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$response = $this->actingAs($user)->post(route('documentation.documents.store'), [
|
$response = $this->actingAs($user)->withSession(['_token' => 'test-token'])->post(route('documentation.documents.store'), $this->csrfPayload([
|
||||||
'documentation_category_id' => $category->id,
|
'documentation_category_id' => $category->id,
|
||||||
'title' => 'Goods In Process',
|
'title' => 'Goods In Process',
|
||||||
'slug' => 'goods-in-process',
|
'slug' => 'goods-in-process',
|
||||||
@@ -50,7 +61,7 @@ class DocumentationModuleTest extends TestCase
|
|||||||
'version' => '1.0.0',
|
'version' => '1.0.0',
|
||||||
'reference' => 'SOP-001',
|
'reference' => 'SOP-001',
|
||||||
'is_featured' => '1',
|
'is_featured' => '1',
|
||||||
]);
|
]));
|
||||||
|
|
||||||
$response->assertRedirect();
|
$response->assertRedirect();
|
||||||
$this->assertDatabaseHas('documentation_documents', [
|
$this->assertDatabaseHas('documentation_documents', [
|
||||||
@@ -72,7 +83,7 @@ class DocumentationModuleTest extends TestCase
|
|||||||
$user = \App\Models\User::factory()->create();
|
$user = \App\Models\User::factory()->create();
|
||||||
$file = UploadedFile::fake()->createWithContent('goods-in.md', "# Goods In\n\n- Scan delivery note\n- Confirm quantities");
|
$file = UploadedFile::fake()->createWithContent('goods-in.md', "# Goods In\n\n- Scan delivery note\n- Confirm quantities");
|
||||||
|
|
||||||
$response = $this->actingAs($user)->post(route('documentation.documents.store'), [
|
$response = $this->actingAs($user)->withSession(['_token' => 'test-token'])->post(route('documentation.documents.store'), $this->csrfPayload([
|
||||||
'title' => 'Goods In Markdown',
|
'title' => 'Goods In Markdown',
|
||||||
'slug' => 'goods-in-markdown',
|
'slug' => 'goods-in-markdown',
|
||||||
'summary' => 'Markdown controlled document.',
|
'summary' => 'Markdown controlled document.',
|
||||||
@@ -80,7 +91,7 @@ class DocumentationModuleTest extends TestCase
|
|||||||
'status' => DocumentationDocument::STATUS_PUBLISHED,
|
'status' => DocumentationDocument::STATUS_PUBLISHED,
|
||||||
'version' => '1.0.0',
|
'version' => '1.0.0',
|
||||||
'attachment' => $file,
|
'attachment' => $file,
|
||||||
]);
|
]));
|
||||||
|
|
||||||
$response->assertRedirect();
|
$response->assertRedirect();
|
||||||
|
|
||||||
@@ -102,7 +113,7 @@ class DocumentationModuleTest extends TestCase
|
|||||||
|
|
||||||
$user = \App\Models\User::factory()->create();
|
$user = \App\Models\User::factory()->create();
|
||||||
|
|
||||||
$response = $this->actingAs($user)->post(route('documentation.documents.store'), [
|
$response = $this->actingAs($user)->withSession(['_token' => 'test-token'])->post(route('documentation.documents.store'), $this->csrfPayload([
|
||||||
'title' => 'Attachment Pack',
|
'title' => 'Attachment Pack',
|
||||||
'slug' => 'attachment-pack',
|
'slug' => 'attachment-pack',
|
||||||
'summary' => 'Document with supporting files.',
|
'summary' => 'Document with supporting files.',
|
||||||
@@ -113,7 +124,7 @@ class DocumentationModuleTest extends TestCase
|
|||||||
UploadedFile::fake()->createWithContent('setup.md', "```php\necho 'ok';\n```"),
|
UploadedFile::fake()->createWithContent('setup.md', "```php\necho 'ok';\n```"),
|
||||||
UploadedFile::fake()->create('process.pdf', 64, 'application/pdf'),
|
UploadedFile::fake()->create('process.pdf', 64, 'application/pdf'),
|
||||||
],
|
],
|
||||||
]);
|
]));
|
||||||
|
|
||||||
$response->assertRedirect();
|
$response->assertRedirect();
|
||||||
|
|
||||||
@@ -129,7 +140,8 @@ class DocumentationModuleTest extends TestCase
|
|||||||
->assertOk();
|
->assertOk();
|
||||||
|
|
||||||
$this->actingAs($user)
|
$this->actingAs($user)
|
||||||
->delete(route('documentation.documents.attachments.destroy', [$document, $attachment]))
|
->withSession(['_token' => 'test-token'])
|
||||||
|
->delete(route('documentation.documents.attachments.destroy', [$document, $attachment]), ['_token' => 'test-token'])
|
||||||
->assertRedirect(route('documentation.documents.edit', $document));
|
->assertRedirect(route('documentation.documents.edit', $document));
|
||||||
|
|
||||||
$this->assertDatabaseMissing('documentation_attachments', ['id' => $attachment->id]);
|
$this->assertDatabaseMissing('documentation_attachments', ['id' => $attachment->id]);
|
||||||
@@ -158,14 +170,14 @@ class DocumentationModuleTest extends TestCase
|
|||||||
{
|
{
|
||||||
$user = \App\Models\User::factory()->create();
|
$user = \App\Models\User::factory()->create();
|
||||||
|
|
||||||
$this->actingAs($user)->post(route('documentation.categories.store'), [
|
$this->actingAs($user)->withSession(['_token' => 'test-token'])->post(route('documentation.categories.store'), $this->csrfPayload([
|
||||||
'name' => 'Policies',
|
'name' => 'Policies',
|
||||||
'slug' => 'policies',
|
'slug' => 'policies',
|
||||||
'colour' => 'emerald',
|
'colour' => 'emerald',
|
||||||
'description' => 'Controlled business policies.',
|
'description' => 'Controlled business policies.',
|
||||||
'sort_order' => 1,
|
'sort_order' => 1,
|
||||||
'is_active' => '1',
|
'is_active' => '1',
|
||||||
])->assertRedirect(route('documentation.categories.index'));
|
]))->assertRedirect(route('documentation.categories.index'));
|
||||||
|
|
||||||
$this->assertDatabaseHas('documentation_categories', [
|
$this->assertDatabaseHas('documentation_categories', [
|
||||||
'name' => 'Policies',
|
'name' => 'Policies',
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Support\Facades\Route;
|
|
||||||
use Modules\Countries\Http\Controllers\Api\CountryLookupController;
|
|
||||||
|
|
||||||
if (class_exists(CountryLookupController::class)) {
|
|
||||||
Route::get('/countries', [CountryLookupController::class, 'index'])->name('api.countries.index');
|
|
||||||
Route::get('/countries/{country:alpha2}', [CountryLookupController::class, 'show'])->name('api.countries.show');
|
|
||||||
}
|
|
||||||
@@ -59,6 +59,7 @@ final class TicketIndex extends Component
|
|||||||
private function tickets(): LengthAwarePaginator
|
private function tickets(): LengthAwarePaginator
|
||||||
{
|
{
|
||||||
$user = Auth::user();
|
$user = Auth::user();
|
||||||
|
$search = trim($this->search);
|
||||||
|
|
||||||
return Ticket::query()
|
return Ticket::query()
|
||||||
->with(['owner', 'agent', 'status', 'priority', 'category', 'project', 'team'])
|
->with(['owner', 'agent', 'status', 'priority', 'category', 'project', 'team'])
|
||||||
@@ -67,11 +68,11 @@ final class TicketIndex extends Component
|
|||||||
->when($this->scope === 'closed', static fn (Builder $query): Builder => $query->whereNotNull('completed_at'))
|
->when($this->scope === 'closed', static fn (Builder $query): Builder => $query->whereNotNull('completed_at'))
|
||||||
->when($this->status !== '', fn (Builder $query): Builder => $query->where('status_id', (int) $this->status))
|
->when($this->status !== '', fn (Builder $query): Builder => $query->where('status_id', (int) $this->status))
|
||||||
->when($this->priority !== '', fn (Builder $query): Builder => $query->where('priority_id', (int) $this->priority))
|
->when($this->priority !== '', fn (Builder $query): Builder => $query->where('priority_id', (int) $this->priority))
|
||||||
->when($this->search !== '', function (Builder $query): Builder {
|
->when($search !== '', static function (Builder $query) use ($search): Builder {
|
||||||
return $query->where(static function (Builder $subQuery): void {
|
return $query->where(static function (Builder $subQuery) use ($search): void {
|
||||||
$subQuery->where('reference', 'like', '%'.$this->search.'%')
|
$subQuery->where('reference', 'like', '%'.$search.'%')
|
||||||
->orWhere('subject', 'like', '%'.$this->search.'%')
|
->orWhere('subject', 'like', '%'.$search.'%')
|
||||||
->orWhere('body', 'like', '%'.$this->search.'%');
|
->orWhere('body', 'like', '%'.$search.'%');
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
->latest('last_activity_at')
|
->latest('last_activity_at')
|
||||||
|
|||||||
@@ -29,8 +29,7 @@ final class TicketPolicy
|
|||||||
public function update(User $user, Ticket $ticket): bool
|
public function update(User $user, Ticket $ticket): bool
|
||||||
{
|
{
|
||||||
return $this->isPrivileged($user)
|
return $this->isPrivileged($user)
|
||||||
|| (int) $ticket->agent_id === (int) $user->id
|
|| ((bool) ($user->helpdesk_agent ?? false) && (int) $ticket->agent_id === (int) $user->id);
|
||||||
|| (int) $ticket->user_id === (int) $user->id;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function comment(User $user, Ticket $ticket): bool
|
public function comment(User $user, Ticket $ticket): bool
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
|
|
||||||
@include('helpdesk::partials.page-header', [
|
@include('helpdesk::partials.page-header', [
|
||||||
'eyebrow' => 'Helpdesk',
|
'eyebrow' => 'Helpdesk',
|
||||||
'title' => 'Create Ticket',
|
'title' => 'Create ticket',
|
||||||
'subtitle' => 'Raise a new support request with priority, project, category, team and full details.',
|
'subtitle' => 'Raise a new support request with priority, project, category, team and full details.',
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ final class AgentAssignmentService
|
|||||||
public function selectAgentId(?int $categoryId): ?int
|
public function selectAgentId(?int $categoryId): ?int
|
||||||
{
|
{
|
||||||
$query = User::query()
|
$query = User::query()
|
||||||
|
->select('users.*')
|
||||||
->where(static function (Builder $query): void {
|
->where(static function (Builder $query): void {
|
||||||
$query->where('helpdesk_agent', true)->orWhere('helpdesk_admin', true);
|
$query->where('helpdesk_agent', true)->orWhere('helpdesk_admin', true);
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -11,6 +11,15 @@
|
|||||||
]"
|
]"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<section class="grid gap-4 sm:grid-cols-2 xl:grid-cols-5">
|
||||||
|
@foreach ([['Companies', $companiesCount], ['Active', $activeCompaniesCount], ['Warehouses', $warehousesCount], ['Prices', $pricesCount], ['Licences', $licensesCount]] as [$label, $value])
|
||||||
|
<article class="rounded-2xl border border-zinc-200 bg-white p-5 shadow-sm dark:border-zinc-800 dark:bg-zinc-900">
|
||||||
|
<p class="text-sm text-zinc-500 dark:text-zinc-400">{{ $label }}</p>
|
||||||
|
<p class="mt-2 text-3xl font-bold text-zinc-900 dark:text-white">{{ $value }}</p>
|
||||||
|
</article>
|
||||||
|
@endforeach
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="grid gap-5 xl:grid-cols-3">
|
<section class="grid gap-5 xl:grid-cols-3">
|
||||||
<article class="rounded-3xl border border-zinc-200 bg-white p-5 shadow-sm dark:border-zinc-800 dark:bg-zinc-900">
|
<article class="rounded-3xl border border-zinc-200 bg-white p-5 shadow-sm dark:border-zinc-800 dark:bg-zinc-900">
|
||||||
<div class="mb-5 flex items-start justify-between gap-4">
|
<div class="mb-5 flex items-start justify-between gap-4">
|
||||||
@@ -79,6 +88,81 @@
|
|||||||
</article>
|
</article>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section class="rounded-3xl border border-zinc-200 bg-white p-5 shadow-sm dark:border-zinc-800 dark:bg-zinc-900">
|
||||||
|
<div class="mb-5 flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
||||||
|
<div>
|
||||||
|
<p class="text-xs font-bold uppercase tracking-[0.28em] text-cyan-500">Pricing</p>
|
||||||
|
|
||||||
|
<h2 class="mt-2 text-xl font-bold text-zinc-900 dark:text-white">
|
||||||
|
Pricing plans
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p class="text-sm text-zinc-500 dark:text-zinc-400">
|
||||||
|
Current platform pricing tiers and administration shortcuts.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<a
|
||||||
|
href="{{ route('platform.pricing.index') }}"
|
||||||
|
wire:navigate
|
||||||
|
class="inline-flex h-10 items-center justify-center gap-2 rounded-xl border border-zinc-200 px-4 text-sm font-bold text-zinc-700 hover:border-cyan-400 hover:text-cyan-700 dark:border-zinc-700 dark:text-zinc-200"
|
||||||
|
>
|
||||||
|
<i class="fa-solid fa-credit-card" aria-hidden="true"></i>
|
||||||
|
View pricing
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="{{ route('platform.prices.index') }}"
|
||||||
|
wire:navigate
|
||||||
|
class="inline-flex h-10 items-center justify-center gap-2 rounded-xl bg-cyan-600 px-4 text-sm font-bold text-white shadow-sm shadow-cyan-600/20 ring-1 ring-cyan-500/30 transition hover:bg-cyan-500"
|
||||||
|
>
|
||||||
|
<i class="fa-solid fa-tags" aria-hidden="true"></i>
|
||||||
|
Manage prices
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid gap-4 md:grid-cols-3">
|
||||||
|
@foreach ($prices as $price)
|
||||||
|
<article class="rounded-2xl border border-zinc-200 p-5 dark:border-zinc-800">
|
||||||
|
<div class="flex items-start justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<h3 class="text-lg font-bold text-zinc-900 dark:text-white">
|
||||||
|
{{ $price->name }}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<p class="mt-1 text-sm text-zinc-500 dark:text-zinc-400">
|
||||||
|
{{ $price->strapline }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if ($price->badge_label)
|
||||||
|
<span class="rounded-full bg-cyan-500/10 px-3 py-1 text-xs font-bold uppercase tracking-wide text-cyan-600 dark:text-cyan-400">
|
||||||
|
{{ $price->badge_label }}
|
||||||
|
</span>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="mt-4 text-2xl font-black text-zinc-900 dark:text-white">
|
||||||
|
£{{ number_format($price->monthly_amount / 100, 0) }}
|
||||||
|
<span class="text-sm font-semibold text-zinc-500">/ month</span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
@if (! empty($price->features))
|
||||||
|
<ul class="mt-4 space-y-2 text-sm text-zinc-600 dark:text-zinc-300">
|
||||||
|
@foreach (array_slice($price->features, 0, 3) as $feature)
|
||||||
|
<li class="flex gap-2">
|
||||||
|
<i class="fa-solid fa-check mt-1 text-cyan-500" aria-hidden="true"></i>
|
||||||
|
<span>{{ $feature }}</span>
|
||||||
|
</li>
|
||||||
|
@endforeach
|
||||||
|
</ul>
|
||||||
|
@endif
|
||||||
|
</article>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="grid gap-5 xl:grid-cols-[360px_minmax(0,1fr)]">
|
<section class="grid gap-5 xl:grid-cols-[360px_minmax(0,1fr)]">
|
||||||
<article class="rounded-3xl border border-zinc-200 bg-white p-5 shadow-sm dark:border-zinc-800 dark:bg-zinc-900">
|
<article class="rounded-3xl border border-zinc-200 bg-white p-5 shadow-sm dark:border-zinc-800 dark:bg-zinc-900">
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<phpunit backupGlobals="false"
|
||||||
|
beStrictAboutTestsThatDoNotTestAnything="false"
|
||||||
|
colors="true"
|
||||||
|
processIsolation="false"
|
||||||
|
stopOnError="false"
|
||||||
|
stopOnFailure="false"
|
||||||
|
cacheDirectory=".phpunit.cache"
|
||||||
|
backupStaticProperties="false">
|
||||||
|
<testsuites>
|
||||||
|
<testsuite name="Browser Test Suite">
|
||||||
|
<directory suffix="Test.php">./tests/Browser</directory>
|
||||||
|
</testsuite>
|
||||||
|
</testsuites>
|
||||||
|
</phpunit>
|
||||||
+58
-14
@@ -5,12 +5,14 @@ namespace Tests;
|
|||||||
use Facebook\WebDriver\Chrome\ChromeOptions;
|
use Facebook\WebDriver\Chrome\ChromeOptions;
|
||||||
use Facebook\WebDriver\Remote\DesiredCapabilities;
|
use Facebook\WebDriver\Remote\DesiredCapabilities;
|
||||||
use Facebook\WebDriver\Remote\RemoteWebDriver;
|
use Facebook\WebDriver\Remote\RemoteWebDriver;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Facades\Artisan;
|
||||||
use Laravel\Dusk\TestCase as BaseTestCase;
|
use Laravel\Dusk\TestCase as BaseTestCase;
|
||||||
use PHPUnit\Framework\Attributes\BeforeClass;
|
use PHPUnit\Framework\Attributes\BeforeClass;
|
||||||
|
|
||||||
abstract class DuskTestCase extends BaseTestCase
|
abstract class DuskTestCase extends BaseTestCase
|
||||||
{
|
{
|
||||||
|
use CreatesApplication;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prepare for Dusk test execution.
|
* Prepare for Dusk test execution.
|
||||||
*/
|
*/
|
||||||
@@ -18,6 +20,7 @@ abstract class DuskTestCase extends BaseTestCase
|
|||||||
public static function prepare(): void
|
public static function prepare(): void
|
||||||
{
|
{
|
||||||
if (! static::runningInSail()) {
|
if (! static::runningInSail()) {
|
||||||
|
static::ensureChromeDriverBinaryAlias();
|
||||||
static::startChromeDriver(['--port=9515']);
|
static::startChromeDriver(['--port=9515']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -27,22 +30,63 @@ abstract class DuskTestCase extends BaseTestCase
|
|||||||
*/
|
*/
|
||||||
protected function driver(): RemoteWebDriver
|
protected function driver(): RemoteWebDriver
|
||||||
{
|
{
|
||||||
$options = (new ChromeOptions)->addArguments(collect([
|
$options = (new ChromeOptions())->addArguments([
|
||||||
$this->shouldStartMaximized() ? '--start-maximized' : '--window-size=1920,1080',
|
'--disable-gpu',
|
||||||
|
'--headless=new',
|
||||||
|
'--window-size=1920,1080',
|
||||||
|
'--no-sandbox',
|
||||||
|
'--disable-dev-shm-usage',
|
||||||
'--disable-search-engine-choice-screen',
|
'--disable-search-engine-choice-screen',
|
||||||
'--disable-smooth-scrolling',
|
]);
|
||||||
])->unless($this->hasHeadlessDisabled(), function (Collection $items) {
|
|
||||||
return $items->merge([
|
|
||||||
'--disable-gpu',
|
|
||||||
'--headless=new',
|
|
||||||
]);
|
|
||||||
})->all());
|
|
||||||
|
|
||||||
return RemoteWebDriver::create(
|
return RemoteWebDriver::create(
|
||||||
$_ENV['DUSK_DRIVER_URL'] ?? env('DUSK_DRIVER_URL') ?? 'http://localhost:9515',
|
$_ENV['DUSK_DRIVER_URL'] ?? 'http://localhost:9515',
|
||||||
DesiredCapabilities::chrome()->setCapability(
|
DesiredCapabilities::chrome()->setCapability(ChromeOptions::CAPABILITY, $options)
|
||||||
ChromeOptions::CAPABILITY, $options
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dusk may install a differently named macOS Apple Silicon binary.
|
||||||
|
* Laravel Dusk still asks for chromedriver-mac-arm, so create that alias safely.
|
||||||
|
*/
|
||||||
|
private static function ensureChromeDriverBinaryAlias(): void
|
||||||
|
{
|
||||||
|
$binDirectory = base_path('vendor/laravel/dusk/bin');
|
||||||
|
$expected = $binDirectory.'/chromedriver-mac-arm';
|
||||||
|
|
||||||
|
if (is_file($expected)) {
|
||||||
|
@chmod($expected, 0755);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! is_dir($binDirectory)) {
|
||||||
|
@mkdir($binDirectory, 0755, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Artisan::call('dusk:chrome-driver');
|
||||||
|
} catch (\Throwable) {
|
||||||
|
// Continue and inspect whatever files are available locally.
|
||||||
|
}
|
||||||
|
|
||||||
|
$candidates = array_values(array_filter(glob($binDirectory.'/chromedriver*') ?: [], static function (string $path) use ($expected): bool {
|
||||||
|
return is_file($path)
|
||||||
|
&& $path !== $expected
|
||||||
|
&& ! str_ends_with($path, '.zip')
|
||||||
|
&& ! str_ends_with($path, '.txt');
|
||||||
|
}));
|
||||||
|
|
||||||
|
usort($candidates, static function (string $left, string $right): int {
|
||||||
|
$score = static fn (string $path): int => str_contains($path, 'mac-arm64') ? 0
|
||||||
|
: (str_contains($path, 'mac-arm') ? 1
|
||||||
|
: (str_contains($path, 'mac') ? 2 : 3));
|
||||||
|
|
||||||
|
return $score($left) <=> $score($right);
|
||||||
|
});
|
||||||
|
|
||||||
|
if ($candidates !== []) {
|
||||||
|
@copy($candidates[0], $expected);
|
||||||
|
@chmod($expected, 0755);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user