1117 lines
47 KiB
PHP
1117 lines
47 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Tests\Feature\Admin;
|
|
|
|
use App\Http\Middleware\ConditionalValidateCsrfToken;
|
|
use App\Models\AcademyAiComparisonResult;
|
|
use App\Models\AcademyCategory;
|
|
use App\Models\AcademyChallenge;
|
|
use App\Models\AcademyChallengeSubmission;
|
|
use App\Models\AcademyCourse;
|
|
use App\Models\AcademyCourseLesson;
|
|
use App\Models\AcademyLesson;
|
|
use App\Models\AcademyLessonBlock;
|
|
use App\Models\AcademyPromptTemplate;
|
|
use App\Models\Artwork;
|
|
use App\Models\User;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Inertia\Testing\AssertableInertia;
|
|
use Tests\TestCase;
|
|
|
|
final class AcademyAdminTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
|
|
$this->withoutMiddleware(ConditionalValidateCsrfToken::class);
|
|
}
|
|
|
|
public function test_admin_can_open_academy_dashboard(): void
|
|
{
|
|
$admin = User::factory()->create(['role' => 'admin']);
|
|
|
|
$this->actingAs($admin)
|
|
->get('/moderation/academy/dashboard')
|
|
->assertOk()
|
|
->assertInertia(fn (AssertableInertia $page) => $page
|
|
->component('Admin/Academy/Dashboard')
|
|
->where('stats.courses', 0)
|
|
->where('stats.lessons', 0)
|
|
->where('stats.prompts', 0));
|
|
}
|
|
|
|
public function test_admin_can_approve_and_reject_challenge_submission(): void
|
|
{
|
|
$admin = User::factory()->create(['role' => 'admin']);
|
|
$user = User::factory()->create();
|
|
$artwork = Artwork::factory()->create(['user_id' => $user->id]);
|
|
$challenge = AcademyChallenge::query()->create([
|
|
'title' => 'Moderated Challenge',
|
|
'slug' => 'moderated-challenge',
|
|
'access_level' => 'free',
|
|
'status' => 'active',
|
|
'active' => true,
|
|
]);
|
|
|
|
$submission = AcademyChallengeSubmission::query()->create([
|
|
'challenge_id' => $challenge->id,
|
|
'user_id' => $user->id,
|
|
'artwork_id' => $artwork->id,
|
|
'moderation_status' => 'pending',
|
|
'submitted_at' => now(),
|
|
]);
|
|
|
|
$this->from('/moderation/academy/submissions')
|
|
->actingAs($admin)
|
|
->post(route('admin.academy.submissions.approve', ['academyChallengeSubmission' => $submission]))
|
|
->assertRedirect('/moderation/academy/submissions');
|
|
|
|
$this->assertDatabaseHas('academy_challenge_submissions', [
|
|
'id' => $submission->id,
|
|
'moderation_status' => 'approved',
|
|
]);
|
|
|
|
$this->from('/moderation/academy/submissions')
|
|
->actingAs($admin)
|
|
->post(route('admin.academy.submissions.reject', ['academyChallengeSubmission' => $submission]))
|
|
->assertRedirect('/moderation/academy/submissions');
|
|
|
|
$this->assertDatabaseHas('academy_challenge_submissions', [
|
|
'id' => $submission->id,
|
|
'moderation_status' => 'rejected',
|
|
]);
|
|
}
|
|
|
|
public function test_admin_can_open_all_academy_modules(): void
|
|
{
|
|
$admin = User::factory()->create(['role' => 'admin']);
|
|
|
|
foreach ([
|
|
'/moderation/academy/dashboard',
|
|
'/moderation/academy/courses',
|
|
'/moderation/academy/categories',
|
|
'/moderation/academy/lessons',
|
|
'/moderation/academy/prompts',
|
|
'/moderation/academy/packs',
|
|
'/moderation/academy/challenges',
|
|
'/moderation/academy/submissions',
|
|
'/moderation/academy/badges',
|
|
] as $path) {
|
|
$this->actingAs($admin)->get($path)->assertOk();
|
|
}
|
|
}
|
|
|
|
public function test_admin_can_open_course_builder(): void
|
|
{
|
|
$admin = User::factory()->create(['role' => 'admin']);
|
|
$course = AcademyCourse::query()->create([
|
|
'title' => 'Builder Course',
|
|
'slug' => 'builder-course',
|
|
'excerpt' => 'Course builder test',
|
|
'access_level' => 'free',
|
|
'difficulty' => 'beginner',
|
|
'status' => 'draft',
|
|
]);
|
|
|
|
$this->actingAs($admin)
|
|
->get(route('admin.academy.courses.builder.edit', ['academyCourse' => $course]))
|
|
->assertOk()
|
|
->assertInertia(fn (AssertableInertia $page) => $page
|
|
->component('Admin/Academy/CourseBuilder')
|
|
->where('course.slug', 'builder-course')
|
|
->where('routes.reorder', route('admin.academy.courses.reorder', ['academyCourse' => $course])));
|
|
}
|
|
|
|
public function test_course_builder_attach_rewrites_lesson_numbers_and_course_order(): void
|
|
{
|
|
$admin = User::factory()->create(['role' => 'admin']);
|
|
$course = AcademyCourse::query()->create([
|
|
'title' => 'Ordering Course',
|
|
'slug' => 'ordering-course',
|
|
'access_level' => 'free',
|
|
'difficulty' => 'beginner',
|
|
'status' => 'draft',
|
|
]);
|
|
|
|
$firstLesson = AcademyLesson::query()->create([
|
|
'title' => 'First Lesson',
|
|
'slug' => 'first-lesson',
|
|
'lesson_number' => 9,
|
|
'course_order' => 9,
|
|
'content' => '<p>Body</p>',
|
|
'difficulty' => 'beginner',
|
|
'access_level' => 'free',
|
|
'lesson_type' => 'article',
|
|
'reading_minutes' => 5,
|
|
'active' => true,
|
|
'published_at' => now()->subMinute(),
|
|
]);
|
|
$secondLesson = AcademyLesson::query()->create([
|
|
'title' => 'Second Lesson',
|
|
'slug' => 'second-lesson',
|
|
'lesson_number' => 8,
|
|
'course_order' => 8,
|
|
'content' => '<p>Body</p>',
|
|
'difficulty' => 'beginner',
|
|
'access_level' => 'free',
|
|
'lesson_type' => 'article',
|
|
'reading_minutes' => 5,
|
|
'active' => true,
|
|
'published_at' => now()->subMinute(),
|
|
]);
|
|
$newLesson = AcademyLesson::query()->create([
|
|
'title' => 'New Lesson',
|
|
'slug' => 'new-lesson',
|
|
'content' => '<p>Body</p>',
|
|
'difficulty' => 'beginner',
|
|
'access_level' => 'free',
|
|
'lesson_type' => 'article',
|
|
'reading_minutes' => 5,
|
|
'active' => true,
|
|
'published_at' => now()->subMinute(),
|
|
]);
|
|
|
|
AcademyCourseLesson::query()->create([
|
|
'course_id' => $course->id,
|
|
'lesson_id' => $firstLesson->id,
|
|
'order_num' => 4,
|
|
'is_required' => true,
|
|
]);
|
|
AcademyCourseLesson::query()->create([
|
|
'course_id' => $course->id,
|
|
'lesson_id' => $secondLesson->id,
|
|
'order_num' => 7,
|
|
'is_required' => true,
|
|
]);
|
|
|
|
$this->actingAs($admin)
|
|
->post(route('admin.academy.courses.lessons.attach', ['academyCourse' => $course]), [
|
|
'lesson_id' => $newLesson->id,
|
|
'order_num' => 12,
|
|
'is_required' => true,
|
|
])
|
|
->assertRedirect();
|
|
|
|
$orderedCourseLessons = AcademyCourseLesson::query()
|
|
->where('course_id', $course->id)
|
|
->orderBy('order_num')
|
|
->get();
|
|
|
|
$this->assertSame([0, 1, 2], $orderedCourseLessons->pluck('order_num')->map(static fn ($value) => (int) $value)->all());
|
|
$this->assertSame([1, 2, 3], AcademyLesson::query()->whereIn('id', [$firstLesson->id, $secondLesson->id, $newLesson->id])->orderBy('course_order')->pluck('course_order')->map(static fn ($value) => (int) $value)->all());
|
|
|
|
$this->assertSame(1, $firstLesson->fresh()->lesson_number);
|
|
$this->assertSame(1, $firstLesson->fresh()->course_order);
|
|
$this->assertSame(2, $secondLesson->fresh()->lesson_number);
|
|
$this->assertSame(2, $secondLesson->fresh()->course_order);
|
|
$this->assertSame(3, $newLesson->fresh()->lesson_number);
|
|
$this->assertSame(3, $newLesson->fresh()->course_order);
|
|
}
|
|
|
|
public function test_course_builder_reorder_rewrites_lesson_numbers_and_course_order(): void
|
|
{
|
|
$admin = User::factory()->create(['role' => 'admin']);
|
|
$course = AcademyCourse::query()->create([
|
|
'title' => 'Reorder Course',
|
|
'slug' => 'reorder-course',
|
|
'access_level' => 'free',
|
|
'difficulty' => 'beginner',
|
|
'status' => 'draft',
|
|
]);
|
|
|
|
$firstLesson = AcademyLesson::query()->create([
|
|
'title' => 'Alpha Lesson',
|
|
'slug' => 'alpha-lesson',
|
|
'lesson_number' => 1,
|
|
'course_order' => 1,
|
|
'content' => '<p>Body</p>',
|
|
'difficulty' => 'beginner',
|
|
'access_level' => 'free',
|
|
'lesson_type' => 'article',
|
|
'reading_minutes' => 5,
|
|
'active' => true,
|
|
'published_at' => now()->subMinute(),
|
|
]);
|
|
$secondLesson = AcademyLesson::query()->create([
|
|
'title' => 'Beta Lesson',
|
|
'slug' => 'beta-lesson',
|
|
'lesson_number' => 2,
|
|
'course_order' => 2,
|
|
'content' => '<p>Body</p>',
|
|
'difficulty' => 'beginner',
|
|
'access_level' => 'free',
|
|
'lesson_type' => 'article',
|
|
'reading_minutes' => 5,
|
|
'active' => true,
|
|
'published_at' => now()->subMinute(),
|
|
]);
|
|
|
|
$firstCourseLesson = AcademyCourseLesson::query()->create([
|
|
'course_id' => $course->id,
|
|
'lesson_id' => $firstLesson->id,
|
|
'order_num' => 0,
|
|
'is_required' => true,
|
|
]);
|
|
$secondCourseLesson = AcademyCourseLesson::query()->create([
|
|
'course_id' => $course->id,
|
|
'lesson_id' => $secondLesson->id,
|
|
'order_num' => 1,
|
|
'is_required' => true,
|
|
]);
|
|
|
|
$this->actingAs($admin)
|
|
->patch(route('admin.academy.courses.reorder', ['academyCourse' => $course]), [
|
|
'sections' => [],
|
|
'lessons' => [
|
|
[
|
|
'id' => $secondCourseLesson->id,
|
|
'order_num' => 0,
|
|
'section_id' => null,
|
|
],
|
|
[
|
|
'id' => $firstCourseLesson->id,
|
|
'order_num' => 1,
|
|
'section_id' => null,
|
|
],
|
|
],
|
|
])
|
|
->assertRedirect();
|
|
|
|
$this->assertSame(1, $secondLesson->fresh()->lesson_number);
|
|
$this->assertSame(1, $secondLesson->fresh()->course_order);
|
|
$this->assertSame(2, $firstLesson->fresh()->lesson_number);
|
|
$this->assertSame(2, $firstLesson->fresh()->course_order);
|
|
$this->assertSame([0, 1], AcademyCourseLesson::query()->where('course_id', $course->id)->orderBy('order_num')->pluck('order_num')->map(static fn ($value) => (int) $value)->all());
|
|
$this->assertSame([$secondLesson->id, $firstLesson->id], AcademyCourseLesson::query()->where('course_id', $course->id)->orderBy('order_num')->pluck('lesson_id')->map(static fn ($value) => (int) $value)->all());
|
|
}
|
|
|
|
public function test_admin_can_open_course_create_form_with_editor_context(): void
|
|
{
|
|
$admin = User::factory()->create(['role' => 'admin']);
|
|
|
|
$this->actingAs($admin)
|
|
->get(route('admin.academy.courses.create'))
|
|
->assertOk()
|
|
->assertInertia(fn (AssertableInertia $page) => $page
|
|
->component('Admin/Academy/CrudForm')
|
|
->where('resource', 'courses')
|
|
->where('editorContext.coverUploadUrl', route('api.studio.academy.lessons.media.upload'))
|
|
->where('editorContext.coverDeleteUrl', route('api.studio.academy.lessons.media.destroy'))
|
|
->where('editorContext.bodyMediaUploadUrl', route('api.studio.academy.lessons.media.upload'))
|
|
->where('record.cover_image_url', null)
|
|
->where('record.teaser_image_url', null));
|
|
}
|
|
|
|
public function test_admin_can_store_prompt_with_ai_model_comparisons(): void
|
|
{
|
|
$admin = User::factory()->create(['role' => 'admin']);
|
|
|
|
$response = $this->actingAs($admin)
|
|
->post(route('admin.academy.prompts.store'), [
|
|
'title' => 'Prompt Comparison Template',
|
|
'slug' => 'prompt-comparison-template',
|
|
'excerpt' => 'Compare the same prompt across different AI models.',
|
|
'prompt' => 'Create a cinematic sci-fi skyline with reflective rain-soaked streets.',
|
|
'negative_prompt' => 'blurry, low detail, text, watermark',
|
|
'usage_notes' => 'Use this when you want reflective city lighting.',
|
|
'workflow_notes' => 'Best after a composition sketch pass.',
|
|
'difficulty' => 'beginner',
|
|
'access_level' => 'free',
|
|
'aspect_ratio' => '16:9',
|
|
'tags' => ['cinematic', 'city'],
|
|
'tool_notes' => [
|
|
[
|
|
'provider' => 'Midjourney',
|
|
'model_name' => 'V7',
|
|
'notes' => 'Produces the strongest mood and lighting with minimal retries.',
|
|
'image_path' => 'academy/lessons/body/aa/bb/prompt-midjourney.webp',
|
|
'thumb_path' => 'academy/lessons/body/aa/bb/prompt-midjourney-thumb.webp',
|
|
'settings' => 'Midjourney V7 on Discord, stylize 200, 16:9 upscale.',
|
|
'strengths' => 'Atmosphere, composition, reflective light.',
|
|
'weaknesses' => 'Can over-stylize signage and crowd details.',
|
|
'best_for' => 'Quick concept frames and wallpaper-ready hero shots.',
|
|
'score' => 9,
|
|
'active' => true,
|
|
],
|
|
],
|
|
'preview_image' => '',
|
|
'featured' => false,
|
|
'prompt_of_week' => false,
|
|
'active' => true,
|
|
'published_at' => now()->subMinute()->toDateTimeString(),
|
|
'seo_title' => '',
|
|
'seo_description' => '',
|
|
]);
|
|
|
|
$prompt = AcademyPromptTemplate::query()->where('slug', 'prompt-comparison-template')->firstOrFail();
|
|
|
|
$response->assertRedirect(route('admin.academy.prompts.edit', ['academyPromptTemplate' => $prompt]));
|
|
$this->assertSame('Midjourney', $prompt->tool_notes[0]['provider'] ?? null);
|
|
$this->assertSame('V7', $prompt->tool_notes[0]['model_name'] ?? null);
|
|
$this->assertSame('Quick concept frames and wallpaper-ready hero shots.', $prompt->tool_notes[0]['best_for'] ?? null);
|
|
$this->assertSame('academy/lessons/body/aa/bb/prompt-midjourney.webp', $prompt->tool_notes[0]['image_path'] ?? null);
|
|
$this->assertSame(9, $prompt->tool_notes[0]['score'] ?? null);
|
|
|
|
$this->actingAs($admin)
|
|
->get(route('admin.academy.prompts.edit', ['academyPromptTemplate' => $prompt]))
|
|
->assertOk()
|
|
->assertInertia(fn (AssertableInertia $page) => $page
|
|
->component('Admin/Academy/CrudForm')
|
|
->where('editorContext.comparisonMediaUploadUrl', route('api.studio.academy.lessons.media.upload'))
|
|
->where('record.tool_notes.0.provider', 'Midjourney')
|
|
->where('record.tool_notes.0.model_name', 'V7')
|
|
->where('record.tool_notes.0.image_path', 'academy/lessons/body/aa/bb/prompt-midjourney.webp')
|
|
->where('record.tool_notes.0.score', 9));
|
|
}
|
|
|
|
public function test_admin_course_edit_form_includes_outline_summary(): void
|
|
{
|
|
$admin = User::factory()->create(['role' => 'admin']);
|
|
$course = AcademyCourse::query()->create([
|
|
'title' => 'Outline Course',
|
|
'slug' => 'outline-course',
|
|
'access_level' => 'free',
|
|
'difficulty' => 'beginner',
|
|
'status' => 'draft',
|
|
]);
|
|
$section = $course->sections()->create([
|
|
'title' => 'Introduction',
|
|
'slug' => 'introduction',
|
|
'order_num' => 0,
|
|
'is_visible' => true,
|
|
]);
|
|
$requiredLesson = AcademyLesson::query()->create([
|
|
'title' => 'Required Lesson',
|
|
'slug' => 'required-lesson',
|
|
'content' => '<p>Body</p>',
|
|
'difficulty' => 'beginner',
|
|
'access_level' => 'free',
|
|
'lesson_type' => 'article',
|
|
'reading_minutes' => 5,
|
|
'active' => true,
|
|
]);
|
|
$optionalLesson = AcademyLesson::query()->create([
|
|
'title' => 'Optional Lesson',
|
|
'slug' => 'optional-lesson',
|
|
'content' => '<p>Body</p>',
|
|
'difficulty' => 'beginner',
|
|
'access_level' => 'free',
|
|
'lesson_type' => 'article',
|
|
'reading_minutes' => 5,
|
|
'active' => true,
|
|
]);
|
|
|
|
AcademyCourseLesson::query()->create([
|
|
'course_id' => $course->id,
|
|
'section_id' => $section->id,
|
|
'lesson_id' => $requiredLesson->id,
|
|
'order_num' => 0,
|
|
'is_required' => true,
|
|
]);
|
|
AcademyCourseLesson::query()->create([
|
|
'course_id' => $course->id,
|
|
'section_id' => null,
|
|
'lesson_id' => $optionalLesson->id,
|
|
'order_num' => 1,
|
|
'is_required' => false,
|
|
]);
|
|
|
|
$this->actingAs($admin)
|
|
->get(route('admin.academy.courses.edit', ['academyCourse' => $course]))
|
|
->assertOk()
|
|
->assertInertia(fn (AssertableInertia $page) => $page
|
|
->component('Admin/Academy/CrudForm')
|
|
->where('editorContext.outlineSummary.section_count', 1)
|
|
->where('editorContext.outlineSummary.visible_section_count', 1)
|
|
->where('editorContext.outlineSummary.lesson_count', 2)
|
|
->where('editorContext.outlineSummary.required_lesson_count', 1)
|
|
->where('editorContext.outlineSummary.unsectioned_lesson_count', 1)
|
|
->where('editorContext.outlineSummary.sections.0.title', 'Introduction')
|
|
->where('editorContext.outlineSummary.sections.0.lesson_count', 1));
|
|
}
|
|
|
|
public function test_admin_can_store_course_with_rich_description_and_media_fields(): void
|
|
{
|
|
$admin = User::factory()->create(['role' => 'admin']);
|
|
|
|
$response = $this->actingAs($admin)
|
|
->post(route('admin.academy.courses.store'), [
|
|
'title' => 'Foundations Course',
|
|
'slug' => 'foundations-course',
|
|
'subtitle' => 'A guided path for Skinbase creators',
|
|
'excerpt' => 'A strong introduction to AI-assisted digital art workflows.',
|
|
'description' => '<h2>What you will learn</h2><p>Prompt structure, workflow cleanup, and publication readiness.</p>',
|
|
'cover_image' => 'academy/lessons/covers/aa/bb/course-cover.webp',
|
|
'teaser_image' => 'academy/lessons/covers/cc/dd/course-teaser.webp',
|
|
'access_level' => 'free',
|
|
'difficulty' => 'beginner',
|
|
'status' => 'published',
|
|
'is_featured' => true,
|
|
'order_num' => 5,
|
|
'estimated_minutes' => 90,
|
|
'published_at' => '',
|
|
'seo_title' => 'Foundations Course',
|
|
'seo_description' => 'A guided Academy course for Skinbase creators.',
|
|
'meta_keywords' => 'academy, ai art, workflow',
|
|
'og_title' => 'Foundations Course',
|
|
'og_description' => 'Learn the fundamentals of AI-assisted digital art.',
|
|
'og_image' => 'academy/lessons/covers/ee/ff/course-og.webp',
|
|
]);
|
|
|
|
$course = AcademyCourse::query()->where('slug', 'foundations-course')->firstOrFail();
|
|
|
|
$response->assertRedirect(route('admin.academy.courses.edit', ['academyCourse' => $course]));
|
|
$this->assertSame('academy/lessons/covers/aa/bb/course-cover.webp', $course->cover_image);
|
|
$this->assertSame('academy/lessons/covers/cc/dd/course-teaser.webp', $course->teaser_image);
|
|
$this->assertStringContainsString('<h2>What you will learn</h2>', (string) $course->description);
|
|
$this->assertTrue((bool) $course->is_featured);
|
|
$this->assertNotNull($course->published_at);
|
|
}
|
|
|
|
public function test_admin_category_update_clears_academy_cache(): void
|
|
{
|
|
$admin = User::factory()->create(['role' => 'admin']);
|
|
$category = AcademyCategory::query()->create([
|
|
'type' => 'lesson',
|
|
'name' => 'Prompting Basics',
|
|
'slug' => 'prompting-basics',
|
|
'order_num' => 10,
|
|
'active' => true,
|
|
]);
|
|
|
|
Cache::put('academy.home', ['stale' => true], 600);
|
|
Cache::put('academy.categories.lesson', ['stale' => true], 600);
|
|
|
|
$this->actingAs($admin)
|
|
->patch(route('admin.academy.categories.update', ['academyCategory' => $category]), [
|
|
'type' => 'lesson',
|
|
'name' => 'Prompting Basics Updated',
|
|
'slug' => 'prompting-basics',
|
|
'description' => 'Updated description',
|
|
'icon' => 'fa-wand-magic-sparkles',
|
|
'order_num' => 11,
|
|
'active' => true,
|
|
])
|
|
->assertRedirect(route('admin.academy.categories.edit', ['academyCategory' => $category]));
|
|
|
|
$this->assertNull(Cache::get('academy.home'));
|
|
$this->assertNull(Cache::get('academy.categories.lesson'));
|
|
}
|
|
|
|
public function test_admin_can_create_a_lesson_with_ai_comparison_block(): void
|
|
{
|
|
$admin = User::factory()->create(['role' => 'admin']);
|
|
|
|
$response = $this->actingAs($admin)
|
|
->post(route('admin.academy.lessons.store'), [
|
|
'title' => 'AI Comparison Lesson',
|
|
'slug' => 'ai-comparison-lesson',
|
|
'excerpt' => 'Testing comparison block creation.',
|
|
'content' => '<p>Lesson body.</p>',
|
|
'difficulty' => 'beginner',
|
|
'access_level' => 'free',
|
|
'lesson_type' => 'article',
|
|
'cover_image' => '',
|
|
'article_cover_image' => 'academy/lessons/covers/article-cover.webp',
|
|
'tags' => ['ai comparison', 'models'],
|
|
'video_url' => '',
|
|
'reading_minutes' => 5,
|
|
'featured' => false,
|
|
'active' => true,
|
|
'published_at' => now()->subMinute()->toDateTimeString(),
|
|
'seo_title' => '',
|
|
'seo_description' => '',
|
|
'blocks' => [
|
|
[
|
|
'type' => 'ai_comparison',
|
|
'title' => 'Same Prompt, Different AI Models',
|
|
'payload' => [
|
|
'title' => 'Same Prompt, Different AI Models',
|
|
'intro' => 'Compare multiple tools.',
|
|
'prompt' => 'A peaceful fantasy forest wallpaper.',
|
|
'negative_prompt' => 'text, watermark',
|
|
'aspect_ratio' => '16:9',
|
|
'criteria' => ['Composition', 'Lighting'],
|
|
],
|
|
'sort_order' => 0,
|
|
'active' => true,
|
|
'comparison_results' => [],
|
|
],
|
|
],
|
|
]);
|
|
|
|
$lesson = AcademyLesson::query()->where('slug', 'ai-comparison-lesson')->firstOrFail();
|
|
|
|
$response->assertRedirect(route('admin.academy.lessons.edit', ['academyLesson' => $lesson]));
|
|
|
|
$this->assertDatabaseHas('academy_lesson_blocks', [
|
|
'lesson_id' => $lesson->id,
|
|
'type' => 'ai_comparison',
|
|
'active' => true,
|
|
]);
|
|
}
|
|
|
|
public function test_admin_can_create_a_lesson_from_markdown(): void
|
|
{
|
|
$admin = User::factory()->create(['role' => 'admin']);
|
|
$markdown = <<<'MD'
|
|
# Cleaner scene direction
|
|
|
|
Use **specific nouns** and keep the camera angle stable.
|
|
|
|
- State the subject
|
|
- Add lighting
|
|
MD;
|
|
|
|
$response = $this->actingAs($admin)
|
|
->post(route('admin.academy.lessons.store'), [
|
|
'title' => 'Markdown Lesson',
|
|
'slug' => 'markdown-lesson',
|
|
'excerpt' => 'Testing markdown lesson creation.',
|
|
'content' => '',
|
|
'content_markdown' => $markdown,
|
|
'difficulty' => 'beginner',
|
|
'access_level' => 'free',
|
|
'lesson_type' => 'article',
|
|
'cover_image' => '',
|
|
'video_url' => '',
|
|
'reading_minutes' => 6,
|
|
'featured' => false,
|
|
'active' => true,
|
|
'published_at' => now()->subMinute()->toDateTimeString(),
|
|
'seo_title' => '',
|
|
'seo_description' => '',
|
|
'blocks' => [],
|
|
]);
|
|
|
|
$lesson = AcademyLesson::query()->where('slug', 'markdown-lesson')->firstOrFail();
|
|
|
|
$response->assertRedirect(route('admin.academy.lessons.edit', ['academyLesson' => $lesson]));
|
|
$this->assertSame($markdown, $lesson->content_markdown);
|
|
$this->assertStringContainsString('<h1>Cleaner scene direction</h1>', (string) $lesson->content);
|
|
$this->assertStringContainsString('<strong>specific nouns</strong>', (string) $lesson->content);
|
|
$this->assertStringContainsString('<li>State the subject</li>', (string) $lesson->content);
|
|
}
|
|
|
|
public function test_admin_can_store_lesson_numbering_fields(): void
|
|
{
|
|
$admin = User::factory()->create(['role' => 'admin']);
|
|
|
|
$response = $this->actingAs($admin)
|
|
->post(route('admin.academy.lessons.store'), [
|
|
'title' => 'Ordered Lesson',
|
|
'slug' => 'ordered-lesson',
|
|
'lesson_number' => 3,
|
|
'course_order' => 3,
|
|
'series_name' => 'AI Art Basics',
|
|
'excerpt' => 'Testing ordering field persistence.',
|
|
'content' => '<p>Lesson body.</p>',
|
|
'tags' => ['workflow', 'academy'],
|
|
'difficulty' => 'beginner',
|
|
'access_level' => 'free',
|
|
'lesson_type' => 'article',
|
|
'cover_image' => '',
|
|
'video_url' => '',
|
|
'reading_minutes' => 5,
|
|
'featured' => false,
|
|
'active' => true,
|
|
'published_at' => now()->subMinute()->toDateTimeString(),
|
|
'seo_title' => '',
|
|
'seo_description' => '',
|
|
'blocks' => [],
|
|
]);
|
|
|
|
$lesson = AcademyLesson::query()->where('slug', 'ordered-lesson')->firstOrFail();
|
|
|
|
$response->assertRedirect(route('admin.academy.lessons.edit', ['academyLesson' => $lesson]));
|
|
$this->assertDatabaseHas('academy_lessons', [
|
|
'id' => $lesson->id,
|
|
'lesson_number' => 3,
|
|
'course_order' => 3,
|
|
'series_name' => 'AI Art Basics',
|
|
]);
|
|
$this->assertSame(['workflow', 'academy'], $lesson->fresh()->tags);
|
|
}
|
|
|
|
public function test_admin_lesson_reading_time_is_calculated_from_content(): void
|
|
{
|
|
$admin = User::factory()->create(['role' => 'admin']);
|
|
$body = '<p>' . implode(' ', array_fill(0, 420, 'prompt')) . '</p>';
|
|
|
|
$response = $this->actingAs($admin)
|
|
->post(route('admin.academy.lessons.store'), [
|
|
'title' => 'Auto Reading Lesson',
|
|
'slug' => 'auto-reading-lesson',
|
|
'excerpt' => 'Estimate reading time from content.',
|
|
'content' => $body,
|
|
'difficulty' => 'beginner',
|
|
'access_level' => 'free',
|
|
'lesson_type' => 'article',
|
|
'cover_image' => '',
|
|
'video_url' => '',
|
|
'reading_minutes' => 1,
|
|
'featured' => false,
|
|
'active' => true,
|
|
'published_at' => now()->subMinute()->toDateTimeString(),
|
|
'seo_title' => '',
|
|
'seo_description' => '',
|
|
'blocks' => [],
|
|
]);
|
|
|
|
$lesson = AcademyLesson::query()->where('slug', 'auto-reading-lesson')->firstOrFail();
|
|
|
|
$response->assertRedirect(route('admin.academy.lessons.edit', ['academyLesson' => $lesson]));
|
|
$this->assertSame(3, (int) $lesson->reading_minutes);
|
|
}
|
|
|
|
public function test_admin_can_attach_lesson_to_courses_from_lesson_form(): void
|
|
{
|
|
$admin = User::factory()->create(['role' => 'admin']);
|
|
$courseA = AcademyCourse::query()->create([
|
|
'title' => 'Foundations',
|
|
'slug' => 'foundations',
|
|
'access_level' => 'free',
|
|
'difficulty' => 'beginner',
|
|
'status' => 'draft',
|
|
]);
|
|
$courseB = AcademyCourse::query()->create([
|
|
'title' => 'Prompt Engineering',
|
|
'slug' => 'prompt-engineering',
|
|
'access_level' => 'free',
|
|
'difficulty' => 'beginner',
|
|
'status' => 'draft',
|
|
]);
|
|
|
|
$response = $this->actingAs($admin)
|
|
->post(route('admin.academy.lessons.store'), [
|
|
'title' => 'Course Attached Lesson',
|
|
'slug' => 'course-attached-lesson',
|
|
'excerpt' => 'Attach this lesson to multiple courses.',
|
|
'content' => '<p>Lesson body.</p>',
|
|
'course_ids' => [$courseA->id, $courseB->id],
|
|
'difficulty' => 'beginner',
|
|
'access_level' => 'free',
|
|
'lesson_type' => 'article',
|
|
'cover_image' => '',
|
|
'video_url' => '',
|
|
'reading_minutes' => 5,
|
|
'featured' => false,
|
|
'active' => true,
|
|
'published_at' => now()->subMinute()->toDateTimeString(),
|
|
'seo_title' => '',
|
|
'seo_description' => '',
|
|
'blocks' => [],
|
|
]);
|
|
|
|
$lesson = AcademyLesson::query()->where('slug', 'course-attached-lesson')->firstOrFail();
|
|
|
|
$response->assertRedirect(route('admin.academy.lessons.edit', ['academyLesson' => $lesson]));
|
|
$this->assertSame(2, AcademyCourseLesson::query()->where('lesson_id', $lesson->id)->count());
|
|
$this->assertDatabaseHas('academy_course_lessons', ['course_id' => $courseA->id, 'lesson_id' => $lesson->id]);
|
|
$this->assertDatabaseHas('academy_course_lessons', ['course_id' => $courseB->id, 'lesson_id' => $lesson->id]);
|
|
$this->assertSame(1, $courseA->fresh()->lessons_count_cache);
|
|
$this->assertSame(1, $courseB->fresh()->lessons_count_cache);
|
|
}
|
|
|
|
public function test_admin_lesson_edit_form_includes_numbering_and_course_outline_context(): void
|
|
{
|
|
$admin = User::factory()->create(['role' => 'admin']);
|
|
$course = AcademyCourse::query()->create([
|
|
'title' => 'Lesson Mapping Course',
|
|
'slug' => 'lesson-mapping-course',
|
|
'access_level' => 'free',
|
|
'difficulty' => 'beginner',
|
|
'status' => 'draft',
|
|
]);
|
|
|
|
AcademyLesson::query()->create([
|
|
'title' => 'Lesson One',
|
|
'slug' => 'lesson-one',
|
|
'lesson_number' => 1,
|
|
'course_order' => 1,
|
|
'content' => '<p>Body</p>',
|
|
'difficulty' => 'beginner',
|
|
'access_level' => 'free',
|
|
'lesson_type' => 'article',
|
|
'reading_minutes' => 5,
|
|
'active' => true,
|
|
'published_at' => now()->subMinute(),
|
|
]);
|
|
|
|
$currentLesson = AcademyLesson::query()->create([
|
|
'title' => 'Current Lesson',
|
|
'slug' => 'current-lesson',
|
|
'content' => '<p>Body</p>',
|
|
'difficulty' => 'beginner',
|
|
'access_level' => 'free',
|
|
'lesson_type' => 'article',
|
|
'reading_minutes' => 5,
|
|
'active' => true,
|
|
'published_at' => now()->subMinute(),
|
|
]);
|
|
|
|
$otherCourseLesson = AcademyLesson::query()->create([
|
|
'title' => 'Lesson Three',
|
|
'slug' => 'lesson-three',
|
|
'lesson_number' => 3,
|
|
'course_order' => 3,
|
|
'content' => '<p>Body</p>',
|
|
'difficulty' => 'beginner',
|
|
'access_level' => 'free',
|
|
'lesson_type' => 'article',
|
|
'reading_minutes' => 5,
|
|
'active' => true,
|
|
'published_at' => now()->subMinute(),
|
|
]);
|
|
|
|
AcademyCourseLesson::query()->create([
|
|
'course_id' => $course->id,
|
|
'lesson_id' => $otherCourseLesson->id,
|
|
'order_num' => 0,
|
|
'is_required' => true,
|
|
]);
|
|
|
|
AcademyCourseLesson::query()->create([
|
|
'course_id' => $course->id,
|
|
'lesson_id' => $currentLesson->id,
|
|
'order_num' => 1,
|
|
'is_required' => true,
|
|
]);
|
|
|
|
$this->actingAs($admin)
|
|
->get(route('admin.academy.lessons.edit', ['academyLesson' => $currentLesson]))
|
|
->assertOk()
|
|
->assertInertia(fn (AssertableInertia $page) => $page
|
|
->component('Admin/Academy/CrudForm')
|
|
->where('resource', 'lessons')
|
|
->where('record.course_ids.0', (string) $course->id)
|
|
->where('editorContext.currentLessonId', $currentLesson->id)
|
|
->where('editorContext.numbering.lesson_number.suggested', 2)
|
|
->where('editorContext.numbering.lesson_number.missing.0', 2)
|
|
->where('editorContext.numbering.course_order.suggested', 2)
|
|
->where('editorContext.courses.0.lesson_count', 2)
|
|
->where('editorContext.courses.0.attach_url', route('admin.academy.courses.lessons.attach', ['academyCourse' => $course]))
|
|
->where('editorContext.courses.0.reorder_url', route('admin.academy.courses.reorder', ['academyCourse' => $course]))
|
|
->where('editorContext.courses.0.lessons.0.order_num', 0)
|
|
->where('editorContext.courses.0.lessons.1.is_current', true)
|
|
->where('editorContext.courses.0.lessons.1.destroy_url', fn ($value) => is_string($value) && str_contains($value, "/moderation/academy/courses/{$course->id}/lessons/")));
|
|
}
|
|
|
|
public function test_admin_can_store_article_cover_image(): void
|
|
{
|
|
$admin = User::factory()->create(['role' => 'admin']);
|
|
|
|
$response = $this->actingAs($admin)
|
|
->post(route('admin.academy.lessons.store'), [
|
|
'title' => 'Lesson With Article Cover',
|
|
'slug' => 'lesson-with-article-cover',
|
|
'excerpt' => 'Testing article cover persistence.',
|
|
'content' => '<p>Lesson body.</p>',
|
|
'tags' => ['cover', 'article'],
|
|
'difficulty' => 'beginner',
|
|
'access_level' => 'free',
|
|
'lesson_type' => 'article',
|
|
'cover_image' => 'academy/lessons/covers/hero-cover.webp',
|
|
'article_cover_image' => 'academy/lessons/covers/article-cover.webp',
|
|
'video_url' => '',
|
|
'reading_minutes' => 5,
|
|
'featured' => false,
|
|
'active' => true,
|
|
'published_at' => now()->subMinute()->toDateTimeString(),
|
|
'seo_title' => '',
|
|
'seo_description' => '',
|
|
'blocks' => [],
|
|
]);
|
|
|
|
$lesson = AcademyLesson::query()->where('slug', 'lesson-with-article-cover')->firstOrFail();
|
|
|
|
$response->assertRedirect(route('admin.academy.lessons.edit', ['academyLesson' => $lesson]));
|
|
$this->assertDatabaseHas('academy_lessons', [
|
|
'id' => $lesson->id,
|
|
'article_cover_image' => 'academy/lessons/covers/article-cover.webp',
|
|
]);
|
|
}
|
|
|
|
public function test_admin_can_add_ai_comparison_result_to_existing_lesson(): void
|
|
{
|
|
$admin = User::factory()->create(['role' => 'admin']);
|
|
$lesson = AcademyLesson::query()->create([
|
|
'title' => 'Existing Lesson',
|
|
'slug' => 'existing-lesson',
|
|
'content' => 'Body',
|
|
'difficulty' => 'beginner',
|
|
'access_level' => 'free',
|
|
'lesson_type' => 'article',
|
|
'active' => true,
|
|
'published_at' => now()->subMinute(),
|
|
]);
|
|
|
|
$this->actingAs($admin)
|
|
->patch(route('admin.academy.lessons.update', ['academyLesson' => $lesson]), [
|
|
'title' => $lesson->title,
|
|
'slug' => $lesson->slug,
|
|
'excerpt' => '',
|
|
'content' => $lesson->content,
|
|
'difficulty' => $lesson->difficulty,
|
|
'access_level' => $lesson->access_level,
|
|
'lesson_type' => $lesson->lesson_type,
|
|
'cover_image' => '',
|
|
'video_url' => '',
|
|
'reading_minutes' => 5,
|
|
'featured' => false,
|
|
'active' => true,
|
|
'published_at' => now()->subMinute()->toDateTimeString(),
|
|
'seo_title' => '',
|
|
'seo_description' => '',
|
|
'blocks' => [
|
|
[
|
|
'type' => 'ai_comparison',
|
|
'title' => 'Same Prompt, Different AI Models',
|
|
'payload' => [
|
|
'title' => 'Same Prompt, Different AI Models',
|
|
'intro' => 'Compare multiple tools.',
|
|
'prompt' => 'A peaceful fantasy forest wallpaper.',
|
|
'negative_prompt' => '',
|
|
'aspect_ratio' => '16:9',
|
|
'criteria' => ['Composition'],
|
|
],
|
|
'sort_order' => 0,
|
|
'active' => true,
|
|
'comparison_results' => [
|
|
[
|
|
'provider' => 'OpenAI',
|
|
'model_name' => 'ChatGPT Images',
|
|
'image_path' => 'academy/lessons/body/aa/bb/example.webp',
|
|
'thumb_path' => 'academy/lessons/body/aa/bb/example-thumb.webp',
|
|
'settings' => 'Default quality',
|
|
'strengths' => 'Strong composition',
|
|
'weaknesses' => 'Slightly over-polished',
|
|
'best_for' => 'Wallpaper concepts',
|
|
'score' => 9,
|
|
'sort_order' => 0,
|
|
'active' => true,
|
|
],
|
|
],
|
|
],
|
|
],
|
|
])
|
|
->assertRedirect(route('admin.academy.lessons.edit', ['academyLesson' => $lesson]));
|
|
|
|
$block = AcademyLessonBlock::query()->where('lesson_id', $lesson->id)->firstOrFail();
|
|
|
|
$this->assertDatabaseHas('academy_ai_comparison_results', [
|
|
'lesson_block_id' => $block->id,
|
|
'provider' => 'OpenAI',
|
|
'model_name' => 'ChatGPT Images',
|
|
'score' => 9,
|
|
]);
|
|
}
|
|
|
|
public function test_admin_markdown_update_regenerates_lesson_html(): void
|
|
{
|
|
$admin = User::factory()->create(['role' => 'admin']);
|
|
$lesson = AcademyLesson::query()->create([
|
|
'title' => 'Markdown Update Lesson',
|
|
'slug' => 'markdown-update-lesson',
|
|
'content' => '<p>Old body</p>',
|
|
'difficulty' => 'beginner',
|
|
'access_level' => 'free',
|
|
'lesson_type' => 'article',
|
|
'active' => true,
|
|
'published_at' => now()->subMinute(),
|
|
]);
|
|
|
|
$markdown = <<<'MD'
|
|
## Prompt checklist
|
|
|
|
1. Start with the scene.
|
|
2. Add the style.
|
|
|
|
> Keep one clear subject.
|
|
MD;
|
|
|
|
$this->actingAs($admin)
|
|
->patch(route('admin.academy.lessons.update', ['academyLesson' => $lesson]), [
|
|
'title' => $lesson->title,
|
|
'slug' => $lesson->slug,
|
|
'excerpt' => '',
|
|
'content' => '',
|
|
'content_markdown' => $markdown,
|
|
'difficulty' => $lesson->difficulty,
|
|
'access_level' => $lesson->access_level,
|
|
'lesson_type' => $lesson->lesson_type,
|
|
'cover_image' => '',
|
|
'video_url' => '',
|
|
'reading_minutes' => 5,
|
|
'featured' => false,
|
|
'active' => true,
|
|
'published_at' => now()->subMinute()->toDateTimeString(),
|
|
'seo_title' => '',
|
|
'seo_description' => '',
|
|
'blocks' => [],
|
|
])
|
|
->assertRedirect(route('admin.academy.lessons.edit', ['academyLesson' => $lesson]));
|
|
|
|
$lesson->refresh();
|
|
|
|
$this->assertSame($markdown, $lesson->content_markdown);
|
|
$this->assertStringContainsString('<h2>Prompt checklist</h2>', (string) $lesson->content);
|
|
$this->assertStringContainsString('<ol>', (string) $lesson->content);
|
|
$this->assertStringContainsString('<blockquote>', (string) $lesson->content);
|
|
}
|
|
|
|
public function test_admin_html_lesson_update_does_not_rewrite_legacy_html_from_generated_markdown(): void
|
|
{
|
|
$admin = User::factory()->create(['role' => 'admin']);
|
|
$legacyHtml = '<h2>Final note</h2><p><strong>Prompting</strong> is a creative skill.</p><ul><li>Keep the best result</li><li>Prepare it</li><li>Present it with care</li></ul>';
|
|
|
|
$lesson = AcademyLesson::query()->create([
|
|
'title' => 'Legacy HTML Lesson',
|
|
'slug' => 'legacy-html-lesson',
|
|
'excerpt' => 'Legacy HTML body.',
|
|
'content' => $legacyHtml,
|
|
'content_markdown' => null,
|
|
'difficulty' => 'beginner',
|
|
'access_level' => 'free',
|
|
'lesson_type' => 'article',
|
|
'active' => true,
|
|
'published_at' => now()->subMinute(),
|
|
]);
|
|
|
|
$this->actingAs($admin)
|
|
->patch(route('admin.academy.lessons.update', ['academyLesson' => $lesson]), [
|
|
'title' => $lesson->title,
|
|
'slug' => $lesson->slug,
|
|
'excerpt' => 'Updated excerpt only.',
|
|
'content' => $legacyHtml,
|
|
'content_markdown' => '',
|
|
'content_source' => 'html',
|
|
'difficulty' => $lesson->difficulty,
|
|
'access_level' => $lesson->access_level,
|
|
'lesson_type' => $lesson->lesson_type,
|
|
'cover_image' => '',
|
|
'video_url' => '',
|
|
'reading_minutes' => 5,
|
|
'featured' => false,
|
|
'active' => true,
|
|
'published_at' => now()->subMinute()->toDateTimeString(),
|
|
'seo_title' => '',
|
|
'seo_description' => '',
|
|
'blocks' => [],
|
|
])
|
|
->assertRedirect(route('admin.academy.lessons.edit', ['academyLesson' => $lesson]));
|
|
|
|
$lesson->refresh();
|
|
|
|
$this->assertSame('Updated excerpt only.', $lesson->excerpt);
|
|
$this->assertSame($legacyHtml, $lesson->content);
|
|
$this->assertNull($lesson->content_markdown);
|
|
}
|
|
|
|
public function test_ai_comparison_score_must_stay_in_range(): void
|
|
{
|
|
$admin = User::factory()->create(['role' => 'admin']);
|
|
$lesson = AcademyLesson::query()->create([
|
|
'title' => 'Validation Lesson',
|
|
'slug' => 'validation-lesson',
|
|
'content' => 'Body',
|
|
'difficulty' => 'beginner',
|
|
'access_level' => 'free',
|
|
'lesson_type' => 'article',
|
|
'active' => true,
|
|
'published_at' => now()->subMinute(),
|
|
]);
|
|
|
|
$this->from(route('admin.academy.lessons.edit', ['academyLesson' => $lesson]))
|
|
->actingAs($admin)
|
|
->patch(route('admin.academy.lessons.update', ['academyLesson' => $lesson]), [
|
|
'title' => $lesson->title,
|
|
'slug' => $lesson->slug,
|
|
'excerpt' => '',
|
|
'content' => $lesson->content,
|
|
'difficulty' => $lesson->difficulty,
|
|
'access_level' => $lesson->access_level,
|
|
'lesson_type' => $lesson->lesson_type,
|
|
'cover_image' => '',
|
|
'video_url' => '',
|
|
'reading_minutes' => 5,
|
|
'featured' => false,
|
|
'active' => true,
|
|
'published_at' => now()->subMinute()->toDateTimeString(),
|
|
'seo_title' => '',
|
|
'seo_description' => '',
|
|
'blocks' => [
|
|
[
|
|
'type' => 'ai_comparison',
|
|
'title' => 'Invalid score block',
|
|
'payload' => [
|
|
'title' => 'Invalid score block',
|
|
'prompt' => 'Prompt',
|
|
'criteria' => ['Composition'],
|
|
],
|
|
'sort_order' => 0,
|
|
'active' => true,
|
|
'comparison_results' => [
|
|
[
|
|
'provider' => 'OpenAI',
|
|
'model_name' => 'ChatGPT Images',
|
|
'image_path' => 'academy/lessons/body/aa/bb/example.webp',
|
|
'score' => 11,
|
|
'sort_order' => 0,
|
|
'active' => true,
|
|
],
|
|
],
|
|
],
|
|
],
|
|
])
|
|
->assertRedirect(route('admin.academy.lessons.edit', ['academyLesson' => $lesson]))
|
|
->assertSessionHasErrors(['blocks.0.comparison_results.0.score']);
|
|
}
|
|
|
|
public function test_lesson_delete_soft_deletes_ai_comparison_children(): void
|
|
{
|
|
$admin = User::factory()->create(['role' => 'admin']);
|
|
$lesson = AcademyLesson::query()->create([
|
|
'title' => 'Delete Lesson',
|
|
'slug' => 'delete-lesson',
|
|
'content' => 'Body',
|
|
'difficulty' => 'beginner',
|
|
'access_level' => 'free',
|
|
'lesson_type' => 'article',
|
|
'active' => true,
|
|
'published_at' => now()->subMinute(),
|
|
]);
|
|
$block = AcademyLessonBlock::query()->create([
|
|
'lesson_id' => $lesson->id,
|
|
'type' => 'ai_comparison',
|
|
'title' => 'Delete Block',
|
|
'payload' => ['title' => 'Delete Block', 'prompt' => 'Prompt'],
|
|
'sort_order' => 0,
|
|
'active' => true,
|
|
]);
|
|
$result = AcademyAiComparisonResult::query()->create([
|
|
'lesson_block_id' => $block->id,
|
|
'provider' => 'OpenAI',
|
|
'model_name' => 'ChatGPT Images',
|
|
'image_path' => 'academy/lessons/body/aa/bb/example.webp',
|
|
'score' => 8,
|
|
'sort_order' => 0,
|
|
'active' => true,
|
|
]);
|
|
|
|
$this->actingAs($admin)
|
|
->delete(route('admin.academy.lessons.destroy', ['academyLesson' => $lesson]))
|
|
->assertRedirect(route('admin.academy.lessons.index'));
|
|
|
|
$this->assertSoftDeleted('academy_lessons', ['id' => $lesson->id]);
|
|
$this->assertSoftDeleted('academy_lesson_blocks', ['id' => $block->id]);
|
|
$this->assertSoftDeleted('academy_ai_comparison_results', ['id' => $result->id]);
|
|
}
|
|
}
|