From 603e893c985f1c258d6cc31f4930bea9ef50eb61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9ia=20Bohner?= Date: Fri, 27 Feb 2026 22:46:53 -0300 Subject: [PATCH 1/7] Add import course --- ...e_name_to_text_on_lms_tests_table.php.stub | 28 +++ src/FilamentLmsServiceProvider.php | 1 + src/Imports/CourseStepsImport.php | 191 ++++++++++++++++++ src/Jobs/ImportCourseFromCsv.php | 37 ++++ src/Models/Video.php | 12 +- .../CourseResource/Pages/ListCourses.php | 67 ++++++ tests/Feature/CourseImportTest.php | 56 +++++ tests/TestCase.php | 7 +- tests/fixtures/course-import-format-a.csv | 4 + tests/fixtures/course-import-format-b.csv | 3 + 10 files changed, 401 insertions(+), 5 deletions(-) create mode 100644 database/migrations/change_name_to_text_on_lms_tests_table.php.stub create mode 100644 src/Imports/CourseStepsImport.php create mode 100644 src/Jobs/ImportCourseFromCsv.php create mode 100644 tests/Feature/CourseImportTest.php create mode 100644 tests/fixtures/course-import-format-a.csv create mode 100644 tests/fixtures/course-import-format-b.csv diff --git a/database/migrations/change_name_to_text_on_lms_tests_table.php.stub b/database/migrations/change_name_to_text_on_lms_tests_table.php.stub new file mode 100644 index 0000000..6ab639a --- /dev/null +++ b/database/migrations/change_name_to_text_on_lms_tests_table.php.stub @@ -0,0 +1,28 @@ +text('name')->change(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('lms_tests', function (Blueprint $table): void { + $table->string('name')->change(); + }); + } +}; diff --git a/src/FilamentLmsServiceProvider.php b/src/FilamentLmsServiceProvider.php index e1d50ae..7d4370d 100644 --- a/src/FilamentLmsServiceProvider.php +++ b/src/FilamentLmsServiceProvider.php @@ -50,6 +50,7 @@ public function configurePackage(Package $package): void 'add_completed_at_to_lms_course_user_table', 'make_material_nullable_in_lms_steps_table', 'backfill_lms_course_user_completed_at_from_step_dates', + 'change_name_to_text_on_lms_tests_table', ]) ->hasCommand(BackfillCourseCompletedAt::class) ->hasInstallCommand(function (InstallCommand $command) { diff --git a/src/Imports/CourseStepsImport.php b/src/Imports/CourseStepsImport.php new file mode 100644 index 0000000..f331709 --- /dev/null +++ b/src/Imports/CourseStepsImport.php @@ -0,0 +1,191 @@ +isEmpty()) { + return; + } + + $first = $rows->first(); + $headers = array_keys($first->toArray()); + $hasStepNameColumn = in_array('step_name', $headers, true); + $hasScriptColumn = in_array('script', $headers, true); + + if (! $hasStepNameColumn && ! in_array('lesson_name', $headers, true)) { + throw new \InvalidArgumentException('Unrecognized CSV format. Expected at least "Step Name" or "Lesson Name" column.'); + } + + $course = Course::create([ + 'name' => $this->courseName, + 'slug' => Str::slug($this->courseName), + 'external_id' => Str::slug($this->courseName, '_'), + ]); + + $lessonOrder = 0; + $lessons = []; + $defaultLesson = null; + + foreach ($rows as $row) { + $stepName = $this->value($row, 'step_name') ?? $this->value($row, 'lesson_name'); + if ($stepName === null || trim((string) $stepName) === '') { + continue; + } + + $stepName = trim((string) $stepName); + + if ($hasStepNameColumn) { + $lessonName = $this->value($row, 'lesson_name'); + $lessonName = $lessonName !== null ? trim((string) $lessonName) : $this->courseName; + if (! isset($lessons[$lessonName])) { + $lessonOrder++; + $lessons[$lessonName] = Lesson::create([ + 'course_id' => $course->id, + 'name' => $lessonName, + 'slug' => Str::slug($lessonName), + 'order' => $lessonOrder, + ]); + } + $lesson = $lessons[$lessonName]; + $videoUrl = $this->value($row, 'url'); + $videoUrl = $videoUrl !== null && trim((string) $videoUrl) !== '' ? trim((string) $videoUrl) : null; + $text = null; + $imageUrl = null; + $linkUrl = null; + } else { + if ($defaultLesson === null) { + $defaultLesson = Lesson::create([ + 'course_id' => $course->id, + 'name' => $this->courseName, + 'slug' => Str::slug($this->courseName), + 'order' => 1, + ]); + } + $lesson = $defaultLesson; + $text = $hasScriptColumn ? $this->value($row, 'script') : null; + $text = $text !== null ? trim((string) $text) : null; + $videoUrl = self::extractUrl($this->value($row, 'video_audio_image')); + $imageUrl = self::extractUrl($this->value($row, 'slides_image')); + $linkUrlRaw = $this->value($row, 'url'); + $linkUrl = $linkUrlRaw !== null && trim((string) $linkUrlRaw) !== '' + ? self::extractUrl($linkUrlRaw) ?? trim((string) $linkUrlRaw) + : null; + } + + [$materialId, $materialType] = $this->createMaterialForStep( + $stepName, + $videoUrl, + $imageUrl, + $linkUrl + ); + + $stepOrder = $lesson->steps()->count() + 1; + $stepSlug = $lesson->slug.'-'.Str::slug($stepName); + + Step::create([ + 'lesson_id' => $lesson->id, + 'order' => $stepOrder, + 'name' => $stepName, + 'slug' => $stepSlug, + 'text' => $text, + 'material_id' => $materialId, + 'material_type' => $materialType, + ]); + } + } + + /** + * Create video, image, or link material (priority: video > image > link). Returns [material_id, material_type]. + * + * @return array{0: int|null, 1: string|null} + */ + protected function createMaterialForStep( + string $stepName, + ?string $videoUrl, + ?string $imageUrl, + ?string $linkUrl + ): array { + if ($videoUrl !== null && $videoUrl !== '') { + $videoUrl = VideoUrlService::convertToEmbedUrl($videoUrl); + $video = Video::create([ + 'name' => $stepName, + 'url' => $videoUrl, + ]); + + return [$video->id, 'video']; + } + + if ($imageUrl !== null && $imageUrl !== '') { + $image = Image::create(['name' => $stepName]); + try { + $image->addMediaFromUrl($imageUrl)->toMediaCollection('image'); + } catch (\Throwable) { + // If URL media add fails, step still has image record + } + + return [$image->id, 'image']; + } + + if ($linkUrl !== null && $linkUrl !== '') { + $link = Link::create([ + 'name' => $stepName, + 'url' => $linkUrl, + ]); + + return [$link->id, 'link']; + } + + return [null, null]; + } + + protected function value(Collection $row, string $key): mixed + { + return $row->get($key); + } +} diff --git a/src/Jobs/ImportCourseFromCsv.php b/src/Jobs/ImportCourseFromCsv.php new file mode 100644 index 0000000..4ef7877 --- /dev/null +++ b/src/Jobs/ImportCourseFromCsv.php @@ -0,0 +1,37 @@ +filePath)) { + return; + } + + try { + Excel::import(new CourseStepsImport($this->courseName), $this->filePath); + } finally { + File::delete($this->filePath); + } + } +} diff --git a/src/Models/Video.php b/src/Models/Video.php index 0efaf41..3879f83 100644 --- a/src/Models/Video.php +++ b/src/Models/Video.php @@ -27,12 +27,18 @@ public function step(): MorphTo return $this->morphTo(Step::class); } - public function getProviderAttribute() + public function getProviderAttribute(): string { - if (str_contains($this->url, 'youtube')) { + $url = $this->url ?? ''; + + if (str_contains($url, 'youtube.com') || str_contains($url, 'youtu.be')) { return 'youtube'; } - return 'vimeo'; + if (str_contains($url, 'vimeo.com') || str_contains($url, 'player.vimeo.com')) { + return 'vimeo'; + } + + return 'external'; } } diff --git a/src/Resources/CourseResource/Pages/ListCourses.php b/src/Resources/CourseResource/Pages/ListCourses.php index 3f8f956..217baec 100644 --- a/src/Resources/CourseResource/Pages/ListCourses.php +++ b/src/Resources/CourseResource/Pages/ListCourses.php @@ -2,8 +2,16 @@ namespace Tapp\FilamentLms\Resources\CourseResource\Pages; +use Filament\Actions\Action; use Filament\Actions\CreateAction; +use Filament\Forms\Components\FileUpload; +use Filament\Forms\Components\TextInput; +use Filament\Notifications\Notification; use Filament\Resources\Pages\ListRecords; +use Illuminate\Http\UploadedFile; +use Illuminate\Support\Facades\Storage; +use Illuminate\Support\Str; +use Tapp\FilamentLms\Jobs\ImportCourseFromCsv; use Tapp\FilamentLms\Resources\CourseResource; class ListCourses extends ListRecords @@ -13,6 +21,65 @@ class ListCourses extends ListRecords protected function getHeaderActions(): array { return [ + Action::make('import_course') + ->label('Import Course') + ->icon('heroicon-o-arrow-up-tray') + ->form([ + FileUpload::make('file') + ->label('CSV file') + ->required() + ->storeFiles(false) + ->acceptedFileTypes([ + 'text/csv', + 'application/csv', + 'text/plain', + 'application/vnd.ms-excel', + ]) + ->maxSize(10240) + ->helperText('Upload a CSV with columns: "Step Name", "Lesson Name", "Url", "Script", "Slides (Image)", "Video (Audio + Image)"'), + TextInput::make('course_name') + ->label('Course name') + ->required() + ->maxLength(255) + ->helperText('Name of the new course.'), + ]) + ->action(function (array $data): void { + $file = $data['file']; + $courseName = trim($data['course_name']); + + if (! $file instanceof UploadedFile) { + Notification::make() + ->title('Import failed') + ->body('Could not read the uploaded file.') + ->danger() + ->send(); + + return; + } + + $storedPath = $file->storeAs( + 'filament-lms/course-imports', + Str::uuid().'.csv' + ); + + if ($storedPath === false) { + Notification::make() + ->title('Import failed') + ->body('Could not store the uploaded file.') + ->danger() + ->send(); + + return; + } + + ImportCourseFromCsv::dispatch($courseName, Storage::path($storedPath)); + + Notification::make() + ->title('Import queued') + ->body("Course \"{$courseName}\" will be imported in the background. You can continue working while the import runs.") + ->success() + ->send(); + }), CreateAction::make(), ]; } diff --git a/tests/Feature/CourseImportTest.php b/tests/Feature/CourseImportTest.php new file mode 100644 index 0000000..c5f2c60 --- /dev/null +++ b/tests/Feature/CourseImportTest.php @@ -0,0 +1,56 @@ + \Tapp\FilamentLms\Tests\TestUser::class]); +}); + +test('course steps import format a creates course lessons steps and videos', function () { + $path = __DIR__.'/../fixtures/course-import-format-a.csv'; + + Excel::import(new CourseStepsImport('Imported Course'), $path); + + $course = Course::where('name', 'Imported Course')->first(); + expect($course)->not->toBeNull(); + expect($course->slug)->toBe('imported-course'); + + $lessons = $course->lessons()->orderBy('order')->get(); + expect($lessons)->toHaveCount(2); + expect($lessons->pluck('name')->toArray())->toBe(['Background', 'Other Lesson']); + + $steps = $course->steps()->orderBy('lms_lessons.order')->orderBy('lms_steps.order')->get(); + expect($steps)->toHaveCount(3); + + $videos = Video::all(); + expect($videos)->toHaveCount(3); + + $firstStep = $steps->first(); + expect($firstStep->material_type)->toBe('video'); + expect($firstStep->material_id)->toBe($videos->first()->id); +}); + +test('course steps import format b creates course with steps and text', function () { + $path = __DIR__.'/../fixtures/course-import-format-b.csv'; + + Excel::import(new CourseStepsImport('Format B Course'), $path); + + $course = Course::where('name', 'Format B Course')->first(); + expect($course)->not->toBeNull(); + + $steps = $course->steps()->orderBy('order')->get(); + expect($steps)->toHaveCount(2); + expect($steps->first()->name)->toBe('Step One'); + expect($steps->first()->text)->toBe('Some script text here'); + expect($steps->get(1)->text)->toBe('More text'); +}); diff --git a/tests/TestCase.php b/tests/TestCase.php index aefac17..674834d 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -8,6 +8,7 @@ use Filament\Support\SupportServiceProvider; use Illuminate\Database\Schema\Blueprint; use Livewire\LivewireServiceProvider; +use Maatwebsite\Excel\ExcelServiceProvider; use Orchestra\Testbench\TestCase as Orchestra; use Spatie\MediaLibrary\MediaLibraryServiceProvider; use Tapp\FilamentFormBuilder\FilamentFormBuilderServiceProvider; @@ -113,7 +114,7 @@ protected function setUpDatabase($app) $table->softDeletes(); }); - // Create lms_steps table + // Create lms_steps table (material nullable to match make_material_nullable migration) $app['db']->connection()->getSchemaBuilder()->create('lms_steps', function (Blueprint $table) { $table->id(); $table->foreignId('lesson_id')->references('id')->on('lms_lessons')->onDelete('cascade'); @@ -121,7 +122,8 @@ protected function setUpDatabase($app) $table->boolean('is_optional')->default(false); $table->string('name'); $table->string('slug'); - $table->morphs('material'); + $table->unsignedBigInteger('material_id')->nullable(); + $table->string('material_type')->nullable(); $table->text('text')->nullable(); $table->foreignId('retry_step_id')->nullable()->constrained('lms_steps')->onDelete('set null'); $table->boolean('require_perfect_score')->default(false); @@ -256,6 +258,7 @@ protected function getPackageProviders($app) $providers = [ LivewireServiceProvider::class, FilamentServiceProvider::class, + ExcelServiceProvider::class, SupportServiceProvider::class, MediaLibraryServiceProvider::class, FilamentLmsServiceProvider::class, diff --git a/tests/fixtures/course-import-format-a.csv b/tests/fixtures/course-import-format-a.csv new file mode 100644 index 0000000..e6141cc --- /dev/null +++ b/tests/fixtures/course-import-format-a.csv @@ -0,0 +1,4 @@ +Step Name,Slide,Lesson Name,url +Intro,1,Background,https://vimeo.com/123 +Part 2,2,Background,https://vimeo.com/456 +Other Lesson Step,1,Other Lesson,https://vimeo.com/789 diff --git a/tests/fixtures/course-import-format-b.csv b/tests/fixtures/course-import-format-b.csv new file mode 100644 index 0000000..f3c9764 --- /dev/null +++ b/tests/fixtures/course-import-format-b.csv @@ -0,0 +1,3 @@ +Lesson Name,Script,Lesson,Slides (Image),Video (Audio + Image),Audio,url +Step One,"Some script text here",,,,, +Step Two,"More text",,,,, From 1ef2b2686da6baa38a698d153cb8d3a60c624975 Mon Sep 17 00:00:00 2001 From: andreia <38911+andreia@users.noreply.github.com> Date: Sat, 28 Feb 2026 01:47:25 +0000 Subject: [PATCH 2/7] Fix styling --- tests/Feature/CourseImportTest.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/Feature/CourseImportTest.php b/tests/Feature/CourseImportTest.php index c5f2c60..7e05658 100644 --- a/tests/Feature/CourseImportTest.php +++ b/tests/Feature/CourseImportTest.php @@ -7,10 +7,7 @@ use Maatwebsite\Excel\Facades\Excel; use Tapp\FilamentLms\Imports\CourseStepsImport; use Tapp\FilamentLms\Models\Course; -use Tapp\FilamentLms\Models\Lesson; -use Tapp\FilamentLms\Models\Step; use Tapp\FilamentLms\Models\Video; -use Tapp\FilamentLms\Tests\TestCase; beforeEach(function () { config(['filament-lms.user_model' => \Tapp\FilamentLms\Tests\TestUser::class]); From 38a9698bd3e39ce0b4107a87944889b8c5ddcdd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9ia=20Bohner?= Date: Fri, 27 Feb 2026 23:13:35 -0300 Subject: [PATCH 3/7] Add transaction --- src/Imports/CourseStepsImport.php | 8 ++++++++ src/Jobs/ImportCourseFromCsv.php | 8 +++----- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/Imports/CourseStepsImport.php b/src/Imports/CourseStepsImport.php index f331709..3215f2b 100644 --- a/src/Imports/CourseStepsImport.php +++ b/src/Imports/CourseStepsImport.php @@ -5,6 +5,7 @@ namespace Tapp\FilamentLms\Imports; use Illuminate\Support\Collection; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Str; use Maatwebsite\Excel\Concerns\ToCollection; use Maatwebsite\Excel\Concerns\WithHeadingRow; @@ -62,6 +63,13 @@ public function collection(Collection $rows): void throw new \InvalidArgumentException('Unrecognized CSV format. Expected at least "Step Name" or "Lesson Name" column.'); } + DB::transaction(function () use ($rows, $hasStepNameColumn, $hasScriptColumn): void { + $this->importRows($rows, $hasStepNameColumn, $hasScriptColumn); + }); + } + + protected function importRows(Collection $rows, bool $hasStepNameColumn, bool $hasScriptColumn): void + { $course = Course::create([ 'name' => $this->courseName, 'slug' => Str::slug($this->courseName), diff --git a/src/Jobs/ImportCourseFromCsv.php b/src/Jobs/ImportCourseFromCsv.php index 4ef7877..ec8749a 100644 --- a/src/Jobs/ImportCourseFromCsv.php +++ b/src/Jobs/ImportCourseFromCsv.php @@ -28,10 +28,8 @@ public function handle(): void return; } - try { - Excel::import(new CourseStepsImport($this->courseName), $this->filePath); - } finally { - File::delete($this->filePath); - } + Excel::import(new CourseStepsImport($this->courseName), $this->filePath); + + File::delete($this->filePath); } } From 98f4f4a274baabc9b08d65df3779ce81eafccf6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9ia=20Bohner?= Date: Fri, 27 Feb 2026 23:34:20 -0300 Subject: [PATCH 4/7] update --- src/Resources/CourseResource/Pages/ListCourses.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Resources/CourseResource/Pages/ListCourses.php b/src/Resources/CourseResource/Pages/ListCourses.php index 217baec..a1d809d 100644 --- a/src/Resources/CourseResource/Pages/ListCourses.php +++ b/src/Resources/CourseResource/Pages/ListCourses.php @@ -24,7 +24,7 @@ protected function getHeaderActions(): array Action::make('import_course') ->label('Import Course') ->icon('heroicon-o-arrow-up-tray') - ->form([ + ->schema([ FileUpload::make('file') ->label('CSV file') ->required() From b6ed3e9ee2e34653c31fd5ed5be9481888bdeb19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9ia=20Bohner?= Date: Mon, 2 Mar 2026 14:33:08 -0300 Subject: [PATCH 5/7] Check for empty lesson name --- src/Imports/CourseStepsImport.php | 3 ++- tests/Feature/CourseImportTest.php | 17 +++++++++++++++++ ...course-import-format-a-empty-lesson-name.csv | 2 ++ 3 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 tests/fixtures/course-import-format-a-empty-lesson-name.csv diff --git a/src/Imports/CourseStepsImport.php b/src/Imports/CourseStepsImport.php index 3215f2b..d321752 100644 --- a/src/Imports/CourseStepsImport.php +++ b/src/Imports/CourseStepsImport.php @@ -90,7 +90,8 @@ protected function importRows(Collection $rows, bool $hasStepNameColumn, bool $h if ($hasStepNameColumn) { $lessonName = $this->value($row, 'lesson_name'); - $lessonName = $lessonName !== null ? trim((string) $lessonName) : $this->courseName; + $lessonNameTrimmed = $lessonName !== null ? trim((string) $lessonName) : ''; + $lessonName = $lessonNameTrimmed !== '' ? $lessonNameTrimmed : $this->courseName; if (! isset($lessons[$lessonName])) { $lessonOrder++; $lessons[$lessonName] = Lesson::create([ diff --git a/tests/Feature/CourseImportTest.php b/tests/Feature/CourseImportTest.php index 7e05658..d115408 100644 --- a/tests/Feature/CourseImportTest.php +++ b/tests/Feature/CourseImportTest.php @@ -7,7 +7,10 @@ use Maatwebsite\Excel\Facades\Excel; use Tapp\FilamentLms\Imports\CourseStepsImport; use Tapp\FilamentLms\Models\Course; +use Tapp\FilamentLms\Models\Lesson; +use Tapp\FilamentLms\Models\Step; use Tapp\FilamentLms\Models\Video; +use Tapp\FilamentLms\Tests\TestCase; beforeEach(function () { config(['filament-lms.user_model' => \Tapp\FilamentLms\Tests\TestUser::class]); @@ -37,6 +40,20 @@ expect($firstStep->material_id)->toBe($videos->first()->id); }); +test('course steps import format a falls back to course name when lesson name is empty', function () { + $path = __DIR__.'/../fixtures/course-import-format-a-empty-lesson-name.csv'; + + Excel::import(new CourseStepsImport('My Course'), $path); + + $course = Course::where('name', 'My Course')->first(); + expect($course)->not->toBeNull(); + + $lessons = $course->lessons()->orderBy('order')->get(); + expect($lessons)->toHaveCount(1); + expect($lessons->first()->name)->toBe('My Course'); + expect($lessons->first()->slug)->toBe('my-course'); +}); + test('course steps import format b creates course with steps and text', function () { $path = __DIR__.'/../fixtures/course-import-format-b.csv'; diff --git a/tests/fixtures/course-import-format-a-empty-lesson-name.csv b/tests/fixtures/course-import-format-a-empty-lesson-name.csv new file mode 100644 index 0000000..9a028ca --- /dev/null +++ b/tests/fixtures/course-import-format-a-empty-lesson-name.csv @@ -0,0 +1,2 @@ +Step Name,Slide,Lesson Name,url +Only Step,1,,https://vimeo.com/123 From 43bb2b995e1ae41589d6668b402b69c5e352f8da Mon Sep 17 00:00:00 2001 From: andreia <38911+andreia@users.noreply.github.com> Date: Mon, 2 Mar 2026 17:33:45 +0000 Subject: [PATCH 6/7] Fix styling --- tests/Feature/CourseImportTest.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/Feature/CourseImportTest.php b/tests/Feature/CourseImportTest.php index d115408..3f3bcf7 100644 --- a/tests/Feature/CourseImportTest.php +++ b/tests/Feature/CourseImportTest.php @@ -7,10 +7,7 @@ use Maatwebsite\Excel\Facades\Excel; use Tapp\FilamentLms\Imports\CourseStepsImport; use Tapp\FilamentLms\Models\Course; -use Tapp\FilamentLms\Models\Lesson; -use Tapp\FilamentLms\Models\Step; use Tapp\FilamentLms\Models\Video; -use Tapp\FilamentLms\Tests\TestCase; beforeEach(function () { config(['filament-lms.user_model' => \Tapp\FilamentLms\Tests\TestUser::class]); From 6c1f627525c2457f0304ea0a9ace30a01a25a597 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9ia=20Bohner?= Date: Mon, 2 Mar 2026 17:23:18 -0300 Subject: [PATCH 7/7] Update video provider --- src/Models/Video.php | 2 +- tests/Feature/VideoProviderTest.php | 47 +++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 tests/Feature/VideoProviderTest.php diff --git a/src/Models/Video.php b/src/Models/Video.php index 3879f83..6c305a5 100644 --- a/src/Models/Video.php +++ b/src/Models/Video.php @@ -31,7 +31,7 @@ public function getProviderAttribute(): string { $url = $this->url ?? ''; - if (str_contains($url, 'youtube.com') || str_contains($url, 'youtu.be')) { + if (str_contains($url, 'youtube.com') || str_contains($url, 'youtube-nocookie.com') || str_contains($url, 'youtu.be')) { return 'youtube'; } diff --git a/tests/Feature/VideoProviderTest.php b/tests/Feature/VideoProviderTest.php new file mode 100644 index 0000000..eb476f3 --- /dev/null +++ b/tests/Feature/VideoProviderTest.php @@ -0,0 +1,47 @@ + 'Test', + 'url' => 'https://www.youtube.com/embed/abc123', + ]); + expect($video->provider)->toBe('youtube'); +}); + +test('video provider returns youtube for youtube-nocookie.com embed URL', function () { + $video = Video::create([ + 'name' => 'Test', + 'url' => 'https://www.youtube-nocookie.com/embed/abc123', + ]); + expect($video->provider)->toBe('youtube'); +}); + +test('video provider returns youtube for youtu.be URL', function () { + $video = Video::create([ + 'name' => 'Test', + 'url' => 'https://youtu.be/abc123', + ]); + expect($video->provider)->toBe('youtube'); +}); + +test('video provider returns vimeo for vimeo.com URL', function () { + $video = Video::create([ + 'name' => 'Test', + 'url' => 'https://vimeo.com/123456', + ]); + expect($video->provider)->toBe('vimeo'); +}); + +test('video provider returns external for unknown URL', function () { + $video = Video::create([ + 'name' => 'Test', + 'url' => 'https://example.com/video', + ]); + expect($video->provider)->toBe('external'); +});