toBeTrue()
->and(Route::has('v5.home'))->toBeFalse()
->and(Route::has('v5.selection.update'))->toBeTrue()
->and(Route::has('v5.clusters.index'))->toBeTrue()
->and(Route::has('v5.clusters.show'))->toBeTrue()
->and(Route::has('v5.clusters.store'))->toBeTrue()
->and(Route::has('v5.clusters.destroy'))->toBeTrue()
->and(Route::has('v5.clusters.servers.store'))->toBeTrue()
->and(Route::has('v5.clusters.servers.update'))->toBeTrue()
->and(Route::has('v5.clusters.servers.check'))->toBeTrue()
->and(Route::has('v5.clusters.servers.bootstrap'))->toBeTrue()
->and(Route::has('v5.clusters.servers.destroy'))->toBeTrue()
->and(Route::has('v5.applications.nginx'))->toBeTrue()
->and(Route::has('v5.applications.refresh'))->toBeTrue()
->and(Route::has('v5.applications.position'))->toBeTrue()
->and(Route::has('v5.caddy-ingresses.position'))->toBeTrue()
->and(Route::has('v5.applications.destroy'))->toBeTrue()
->and(Route::has('v5.resource-connections.store'))->toBeTrue()
->and(Route::has('v5.resource-connections.update'))->toBeTrue()
->and(Route::has('v5.resource-connections.destroy'))->toBeTrue()
->and(Route::has('v5.realtime-test'))->toBeTrue()
->and(Route::has('v5.realtime-test.broadcast'))->toBeTrue()
->and(Route::has('v5.coolify.version'))->toBeFalse()
->and(Route::has('v5.coolify.bootstrap'))->toBeFalse();
});
it('uses separated v5 middleware groups', function () {
$kernel = app(Kernel::class);
$reflection = new ReflectionClass($kernel);
$property = $reflection->getProperty('middlewareGroups');
$property->setAccessible(true);
$groups = $property->getValue($kernel);
expect($groups)->toHaveKey('v5.web')
->and($groups)->toHaveKey('v5.authenticated')
->and($groups['v5.web'])->toContain(HandleInertiaRequests::class)
->and($groups['v5.web'])->not->toContain(CheckForcePasswordReset::class)
->and($groups['v5.web'])->not->toContain(DecideWhatToDoWithUser::class)
->and($groups['v5.authenticated'])->toContain('auth')
->and($groups['v5.authenticated'])->toContain('verified')
->and($groups['v5.authenticated'])->toContain(EnsureCurrentTeam::class);
});
it('reuses existing projects instead of creating v5 projects', function () {
expect(file_exists(database_path('migrations/2026_06_04_050157_v5_create_projects_table.php')))->toBeFalse()
->and(file_exists(app_path('Models/V5/Project.php')))->toBeFalse();
});
it('keeps long v5 application metadata inside the canvas card', function () {
$dashboardSource = file_get_contents(resource_path('js/v5/Pages/Dashboard.tsx'));
expect($dashboardSource)
->toContain('overflow-hidden')
->and($dashboardSource)->toContain('grid grid-cols-[auto_minmax(0,1fr)]')
->and($dashboardSource)->toContain('truncate text-right font-medium')
->and($dashboardSource)->toContain('truncate text-right font-mono');
});
it('does not render v5 application status messages on dashboard cards', function () {
$dashboardSource = file_get_contents(resource_path('js/v5/Pages/Dashboard.tsx'));
expect($dashboardSource)
->not->toContain('{application.statusMessage && (')
->not->toContain('{application.statusMessage}
');
});
it('shows a dashboard refresh button next to the center button', function () {
$dashboardSource = file_get_contents(resource_path('js/v5/Pages/Dashboard.tsx'));
expect($dashboardSource)
->toContain('onClick={() => centerOnCanvasNodes()}')
->toContain('onClick={() => void refreshApplications()}')
->toContain("isRefreshing ? 'Refreshing…' : 'Refresh state'");
});
it('subscribes the v5 dashboard canvas to automatic resource status updates', function () {
$dashboardSource = file_get_contents(resource_path('js/v5/Pages/Dashboard.tsx'));
expect($dashboardSource)
->toContain('currentTeam = null')
->toContain("channel.listen('.v5.canvas.resource.updated'")
->toContain('setApplications((currentApplications) =>')
->toContain('setIngresses((currentIngresses) =>')
->toContain('Waiting for window.Echo before subscribing to canvas updates')
->toContain("channel.listen('.v5.canvas.resource.updated'")
->not->toContain("fetch('/v5/canvas-state'");
});
it('allows zooming the v5 dashboard canvas with buttons and pinch gestures', function () {
$dashboardSource = file_get_contents(resource_path('js/v5/Pages/Dashboard.tsx'));
expect($dashboardSource)
->toContain('zoom: number;')
->toContain('MIN_CANVAS_ZOOM')
->toContain('MAX_CANVAS_ZOOM')
->toContain('PINCH_CANVAS_ZOOM_STEP')
->toContain('zoomCanvas(')
->toContain('zoomCanvas(event.deltaY < 0 ? 1 : -1, PINCH_CANVAS_ZOOM_STEP')
->toContain('onWheel={handleCanvasWheel}')
->toContain('event.ctrlKey')
->toContain('aria-label="Zoom out"')
->toContain('aria-label="Zoom in"')
->toContain('Math.round(viewport.zoom * 100)')
->toContain('scale(${viewport.zoom})');
});
it('renders draggable connector dots on v5 application canvas cards', function () {
$dashboardSource = file_get_contents(resource_path('js/v5/Pages/Dashboard.tsx'));
expect($dashboardSource)
->toContain("type ConnectorSide = 'top' | 'right' | 'bottom' | 'left';")
->toContain('application-connector')
->toContain('data-connector-side={side}')
->toContain('startConnectionDrag(event, application.id, side)')
->toContain('