Implement academy analytics, billing, and web stories updates
This commit is contained in:
@@ -22,6 +22,7 @@ use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Inertia\Testing\AssertableInertia;
|
||||
use Tests\TestCase;
|
||||
|
||||
@@ -500,8 +501,18 @@ final class AcademyFeatureTest extends TestCase
|
||||
'excerpt' => 'Locked preview.',
|
||||
'prompt' => 'SECRET PREMIUM PROMPT STRING',
|
||||
'negative_prompt' => 'SECRET NEGATIVE STRING',
|
||||
'usage_notes' => 'SECRET WORKFLOW NOTE',
|
||||
'difficulty' => 'beginner',
|
||||
'access_level' => 'creator',
|
||||
'tool_notes' => [[
|
||||
'display_type' => 'soft studio version',
|
||||
'provider' => 'ChatGPT',
|
||||
'model_name' => '4o Image',
|
||||
'image_path' => 'academy/lessons/body/cc/dd/chatgpt-comparison.webp',
|
||||
'settings' => 'SECRET SETTINGS STRING',
|
||||
'best_for' => 'SECRET BEST FOR STRING',
|
||||
'active' => true,
|
||||
]],
|
||||
'active' => true,
|
||||
'published_at' => now()->subMinute(),
|
||||
]);
|
||||
@@ -510,10 +521,21 @@ final class AcademyFeatureTest extends TestCase
|
||||
->assertOk()
|
||||
->assertDontSee('SECRET PREMIUM PROMPT STRING')
|
||||
->assertDontSee('SECRET NEGATIVE STRING')
|
||||
->assertDontSee('SECRET WORKFLOW NOTE')
|
||||
->assertDontSee('SECRET SETTINGS STRING')
|
||||
->assertDontSee('SECRET BEST FOR STRING')
|
||||
->assertInertia(fn (AssertableInertia $page) => $page
|
||||
->where('item.locked', true)
|
||||
->where('item.prompt', null)
|
||||
->where('item.negative_prompt', null));
|
||||
->where('item.negative_prompt', null)
|
||||
->where('item.access_requirement', 'Requires Creator or Pro access.')
|
||||
->where('item.unlock_heading', 'Unlock the full Creator prompt.')
|
||||
->where('item.tool_notes', [])
|
||||
->where('item.public_examples.0.provider', 'ChatGPT')
|
||||
->where('item.public_examples.0.model_name', '4o Image')
|
||||
->where('item.public_examples.0.image_path', 'academy/lessons/body/cc/dd/chatgpt-comparison.webp')
|
||||
->where('seo.json_ld.0.isAccessibleForFree', false)
|
||||
->where('seo.json_ld.0.hasPart.cssSelector', '.academy-paywalled-content'));
|
||||
|
||||
$version = app(HandleInertiaRequests::class)
|
||||
->version(Request::create(route('academy.prompts.show', ['slug' => $prompt->slug]), 'GET'));
|
||||
@@ -527,8 +549,11 @@ final class AcademyFeatureTest extends TestCase
|
||||
->assertJsonPath('props.item.locked', true)
|
||||
->assertJsonPath('props.item.prompt', null)
|
||||
->assertJsonPath('props.item.negative_prompt', null)
|
||||
->assertJsonPath('props.item.tool_notes', [])
|
||||
->assertJsonPath('props.item.public_examples.0.provider', 'ChatGPT')
|
||||
->assertDontSee('SECRET PREMIUM PROMPT STRING')
|
||||
->assertDontSee('SECRET NEGATIVE STRING');
|
||||
->assertDontSee('SECRET NEGATIVE STRING')
|
||||
->assertDontSee('SECRET SETTINGS STRING');
|
||||
}
|
||||
|
||||
public function test_authorized_user_can_view_premium_prompt(): void
|
||||
@@ -563,10 +588,236 @@ final class AcademyFeatureTest extends TestCase
|
||||
->assertInertia(fn (AssertableInertia $page) => $page
|
||||
->where('item.locked', false)
|
||||
->where('item.prompt', 'VISIBLE PREMIUM PROMPT')
|
||||
->where('item.public_examples.0.provider', 'ChatGPT')
|
||||
->where('item.tool_notes.0.provider', 'ChatGPT')
|
||||
->where('item.tool_notes.0.model_name', '4o Image')
|
||||
->where('item.tool_notes.0.image_path', 'academy/lessons/body/cc/dd/chatgpt-comparison.webp')
|
||||
->where('item.tool_notes.0.score', 8));
|
||||
->where('item.tool_notes.0.score', 8)
|
||||
->where('seo.json_ld.0.isAccessibleForFree', false)
|
||||
->where('seo.json_ld.0.hasPart.cssSelector', '.academy-paywalled-content'));
|
||||
}
|
||||
|
||||
public function test_prompt_payload_exposes_responsive_preview_and_comparison_images(): void
|
||||
{
|
||||
config()->set('uploads.object_storage.disk', 's3');
|
||||
Storage::fake('s3');
|
||||
|
||||
Storage::disk('s3')->put('academy-prompts/previews/sticker-pack.webp', 'preview');
|
||||
Storage::disk('s3')->put('academy-prompts/previews/sticker-pack-thumb.webp', 'preview-thumb');
|
||||
Storage::disk('s3')->put('academy-prompts/previews/sticker-pack-md.webp', 'preview-medium');
|
||||
Storage::disk('s3')->put('academy/lessons/body/cc/dd/chatgpt-comparison.webp', 'comparison');
|
||||
Storage::disk('s3')->put('academy/lessons/body/cc/dd/chatgpt-comparison-thumb.webp', 'comparison-thumb');
|
||||
Storage::disk('s3')->put('academy/lessons/body/cc/dd/chatgpt-comparison-md.webp', 'comparison-medium');
|
||||
|
||||
$prompt = AcademyPromptTemplate::query()->create([
|
||||
'title' => 'Responsive Prompt Media',
|
||||
'slug' => 'responsive-prompt-media',
|
||||
'excerpt' => 'Prompt with responsive preview assets.',
|
||||
'prompt' => 'Create a chibi emoji sticker collection with bright outlines.',
|
||||
'difficulty' => 'beginner',
|
||||
'access_level' => 'free',
|
||||
'preview_image' => 'academy-prompts/previews/sticker-pack.webp',
|
||||
'tool_notes' => [[
|
||||
'display_type' => 'sticker pack',
|
||||
'provider' => 'ChatGPT',
|
||||
'model_name' => '4o Image',
|
||||
'image_path' => 'academy/lessons/body/cc/dd/chatgpt-comparison.webp',
|
||||
'thumb_path' => 'academy/lessons/body/cc/dd/chatgpt-comparison-thumb.webp',
|
||||
'settings' => 'Square canvas, bold outline, soft pastel background.',
|
||||
'best_for' => 'Sticker-ready mascot packs.',
|
||||
'active' => true,
|
||||
]],
|
||||
'active' => true,
|
||||
'published_at' => now()->subMinute(),
|
||||
]);
|
||||
|
||||
$this->get(route('academy.prompts.show', ['slug' => $prompt->slug]))
|
||||
->assertOk()
|
||||
->assertInertia(fn (AssertableInertia $page) => $page
|
||||
->where('item.preview_image_thumb', fn ($value) => is_string($value) && str_contains($value, 'academy-prompts/previews/sticker-pack-thumb.webp'))
|
||||
->where('item.preview_image_srcset', fn ($value) => is_string($value) && str_contains($value, 'academy-prompts/previews/sticker-pack-thumb.webp 480w') && str_contains($value, 'academy-prompts/previews/sticker-pack-md.webp 960w'))
|
||||
->where('item.public_examples.0.thumb_path', 'academy/lessons/body/cc/dd/chatgpt-comparison-thumb.webp')
|
||||
->where('item.public_examples.0.image_srcset', fn ($value) => is_string($value) && str_contains($value, 'chatgpt-comparison-thumb.webp 480w') && str_contains($value, 'chatgpt-comparison-md.webp 960w'))
|
||||
->where('item.tool_notes.0.thumb_path', 'academy/lessons/body/cc/dd/chatgpt-comparison-thumb.webp')
|
||||
->where('item.tool_notes.0.image_srcset', fn ($value) => is_string($value) && str_contains($value, 'chatgpt-comparison-thumb.webp 480w') && str_contains($value, 'chatgpt-comparison-md.webp 960w')));
|
||||
}
|
||||
|
||||
public function test_authorized_user_receives_active_advanced_prompt_metadata(): void
|
||||
{
|
||||
$prompt = AcademyPromptTemplate::query()->create([
|
||||
'title' => 'Advanced Creator Prompt',
|
||||
'slug' => 'advanced-creator-prompt',
|
||||
'excerpt' => 'Full prompt visible.',
|
||||
'prompt' => 'VISIBLE PREMIUM PROMPT FOR [CITY_NAME]',
|
||||
'negative_prompt' => 'VISIBLE NEGATIVE PROMPT',
|
||||
'documentation' => [
|
||||
'summary' => 'Advanced summary visible to everyone.',
|
||||
'how_to_use' => ['Collect data', 'Prepare prompt'],
|
||||
'best_for' => ['city wallpapers'],
|
||||
],
|
||||
'placeholders' => [
|
||||
[
|
||||
'key' => 'CITY_NAME',
|
||||
'label' => 'City name',
|
||||
'required' => true,
|
||||
'example' => 'Paris',
|
||||
'type' => 'text',
|
||||
],
|
||||
],
|
||||
'helper_prompts' => [
|
||||
[
|
||||
'title' => 'Collect city data',
|
||||
'description' => 'Gather landmark and climate data.',
|
||||
'prompt' => 'Collect city data for [CITY_NAME].',
|
||||
'expected_output' => 'json',
|
||||
'active' => true,
|
||||
],
|
||||
[
|
||||
'title' => 'Inactive helper',
|
||||
'description' => 'Should stay hidden publicly.',
|
||||
'prompt' => 'Hidden helper prompt.',
|
||||
'expected_output' => 'text',
|
||||
'active' => false,
|
||||
],
|
||||
],
|
||||
'prompt_variants' => [
|
||||
[
|
||||
'title' => 'Image-safe version',
|
||||
'slug' => 'image-safe-version',
|
||||
'description' => 'Safer for image models.',
|
||||
'prompt' => 'VISIBLE IMAGE SAFE PROMPT',
|
||||
'negative_prompt' => 'VISIBLE VARIANT NEGATIVE',
|
||||
'recommended' => true,
|
||||
'recommended_for' => ['general image generation'],
|
||||
'risk_notes' => ['Icons may still be abstract'],
|
||||
'active' => true,
|
||||
],
|
||||
[
|
||||
'title' => 'Inactive variant',
|
||||
'slug' => 'inactive-variant',
|
||||
'description' => 'Should stay hidden publicly.',
|
||||
'prompt' => 'HIDDEN VARIANT PROMPT',
|
||||
'active' => false,
|
||||
],
|
||||
],
|
||||
'difficulty' => 'beginner',
|
||||
'access_level' => 'creator',
|
||||
'active' => true,
|
||||
'published_at' => now()->subMinute(),
|
||||
]);
|
||||
|
||||
$creator = User::factory()->create(['role' => 'academy_creator']);
|
||||
|
||||
$this->actingAs($creator)
|
||||
->get(route('academy.prompts.show', ['slug' => $prompt->slug]))
|
||||
->assertOk()
|
||||
->assertInertia(fn (AssertableInertia $page) => $page
|
||||
->where('item.locked', false)
|
||||
->where('item.documentation.summary', 'Advanced summary visible to everyone.')
|
||||
->where('item.placeholders.0.key', 'CITY_NAME')
|
||||
->where('item.has_placeholder_inputs', true)
|
||||
->where('item.has_helper_prompts', true)
|
||||
->where('item.has_prompt_variants', true)
|
||||
->has('item.helper_prompts', 1)
|
||||
->where('item.helper_prompts.0.title', 'Collect city data')
|
||||
->has('item.prompt_variants', 1)
|
||||
->where('item.prompt_variants.0.title', 'Image-safe version')
|
||||
);
|
||||
}
|
||||
|
||||
public function test_locked_prompt_still_exposes_documentation_and_placeholders_but_hides_helper_prompts_and_variants(): void
|
||||
{
|
||||
$prompt = AcademyPromptTemplate::query()->create([
|
||||
'title' => 'Locked Advanced Prompt',
|
||||
'slug' => 'locked-advanced-prompt',
|
||||
'excerpt' => 'Locked prompt with public guidance.',
|
||||
'prompt' => 'SECRET ADVANCED PROMPT FOR [CITY_NAME]',
|
||||
'negative_prompt' => 'SECRET ADVANCED NEGATIVE',
|
||||
'documentation' => [
|
||||
'summary' => 'Public-facing overview.',
|
||||
'how_to_use' => ['Choose a city', 'Collect climate data'],
|
||||
'tips' => ['Use real data'],
|
||||
],
|
||||
'placeholders' => [
|
||||
[
|
||||
'key' => 'CITY_NAME',
|
||||
'label' => 'City name',
|
||||
'required' => true,
|
||||
'example' => 'Paris',
|
||||
'type' => 'text',
|
||||
],
|
||||
],
|
||||
'helper_prompts' => [
|
||||
[
|
||||
'title' => 'Collect city data',
|
||||
'description' => 'Hidden behind access.',
|
||||
'prompt' => 'SECRET HELPER PROMPT',
|
||||
'expected_output' => 'json',
|
||||
'active' => true,
|
||||
],
|
||||
],
|
||||
'prompt_variants' => [
|
||||
[
|
||||
'title' => 'Image-safe version',
|
||||
'description' => 'Hidden behind access.',
|
||||
'prompt' => 'SECRET VARIANT PROMPT',
|
||||
'negative_prompt' => 'SECRET VARIANT NEGATIVE',
|
||||
'active' => true,
|
||||
],
|
||||
],
|
||||
'difficulty' => 'beginner',
|
||||
'access_level' => 'creator',
|
||||
'active' => true,
|
||||
'published_at' => now()->subMinute(),
|
||||
]);
|
||||
|
||||
$this->get(route('academy.prompts.show', ['slug' => $prompt->slug]))
|
||||
->assertOk()
|
||||
->assertDontSee('SECRET ADVANCED PROMPT')
|
||||
->assertDontSee('SECRET HELPER PROMPT')
|
||||
->assertDontSee('SECRET VARIANT PROMPT')
|
||||
->assertInertia(fn (AssertableInertia $page) => $page
|
||||
->where('item.locked', true)
|
||||
->where('item.prompt', null)
|
||||
->where('item.documentation.summary', 'Public-facing overview.')
|
||||
->where('item.placeholders.0.key', 'CITY_NAME')
|
||||
->where('item.has_placeholder_inputs', true)
|
||||
->where('item.has_helper_prompts', true)
|
||||
->where('item.has_prompt_variants', true)
|
||||
->where('item.helper_prompts', [])
|
||||
->where('item.prompt_variants', []));
|
||||
}
|
||||
|
||||
public function test_prompt_without_placeholder_tokens_marks_placeholder_inputs_as_hidden(): void
|
||||
{
|
||||
$prompt = AcademyPromptTemplate::query()->create([
|
||||
'title' => 'Descriptor Only Prompt',
|
||||
'slug' => 'descriptor-only-prompt',
|
||||
'excerpt' => 'Has descriptive placeholder cards but no input tokens in the prompt.',
|
||||
'prompt' => 'Create a calm Roman rooftop garden scene at sunrise.',
|
||||
'documentation' => [
|
||||
'summary' => 'A fixed prompt with no user-substituted variables.',
|
||||
],
|
||||
'placeholders' => [
|
||||
[
|
||||
'key' => 'CITY_STYLE',
|
||||
'label' => 'City style',
|
||||
'description' => 'Editorial guidance only.',
|
||||
'example' => 'Historic Rome rooftop terrace with distant domes',
|
||||
'type' => 'string',
|
||||
],
|
||||
],
|
||||
'difficulty' => 'beginner',
|
||||
'access_level' => 'free',
|
||||
'active' => true,
|
||||
'published_at' => now()->subMinute(),
|
||||
]);
|
||||
|
||||
$this->get(route('academy.prompts.show', ['slug' => $prompt->slug]))
|
||||
->assertOk()
|
||||
->assertInertia(fn (AssertableInertia $page) => $page
|
||||
->where('item.placeholders.0.key', 'CITY_STYLE')
|
||||
->where('item.has_placeholder_inputs', false));
|
||||
}
|
||||
|
||||
public function test_public_lesson_payload_includes_active_ai_comparison_block_and_hides_inactive_results(): void
|
||||
@@ -1008,6 +1259,7 @@ final class AcademyFeatureTest extends TestCase
|
||||
->assertDontSee((string) $prompt->negative_prompt)
|
||||
->assertInertia(fn (AssertableInertia $page) => $page
|
||||
->where('item.locked', true)
|
||||
->where('item.access_requirement', $prompt->access_level === 'pro' ? 'Requires Pro access.' : 'Requires Creator or Pro access.')
|
||||
->where('item.prompt', null)
|
||||
->where('item.negative_prompt', null));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user