foundation(); Sanctum::actingAs($designer); $this->getJson('/api/v1/block-registry')->assertOk()->assertJsonFragment(['type' => 'single_choice']); $this->postJson($this->blocksUrl($course, $version, $lesson), [ 'type' => 'heading', 'schemaVersion' => 1, 'data' => ['text' => 'Welcome', 'level' => 2], ])->assertCreated()->assertJsonPath('data.revision', 1)->assertJsonPath('data.position', 1); } public function test_registry_rejects_unknown_type_and_invalid_payload(): void { [$designer, $course, $version, $lesson] = $this->foundation(); Sanctum::actingAs($designer); $this->postJson($this->blocksUrl($course, $version, $lesson), [ 'type' => 'unknown', 'schemaVersion' => 1, 'data' => ['value' => true], ])->assertUnprocessable()->assertJsonValidationErrors('type'); $this->postJson($this->blocksUrl($course, $version, $lesson), [ 'type' => 'heading', 'schemaVersion' => 1, 'data' => ['text' => '', 'level' => 9], ])->assertUnprocessable()->assertJsonValidationErrors(['text', 'level']); $this->postJson($this->blocksUrl($course, $version, $lesson), [ 'type' => 'heading', 'schemaVersion' => 1, 'data' => ['text' => 'Valid', 'level' => 2, 'script' => 'unknown'], ])->assertUnprocessable()->assertJsonValidationErrors('data.script'); } public function test_registry_exposes_complete_phase_six_contract_and_defaults_validate(): void { [$designer, $course, $version, $lesson] = $this->foundation(); Sanctum::actingAs($designer); $registry = $this->getJson('/api/v1/block-registry')->assertOk()->json('data'); $required = ['heading', 'text', 'quote', 'key_point', 'divider', 'button', 'image', 'gallery', 'video', 'audio', 'document', 'embed', 'flashcard', 'accordion', 'tabs', 'timeline', 'steps', 'process', 'checklist', 'section', 'columns', 'controlled_grid']; $this->assertEqualsCanonicalizing($required, collect($registry)->whereNotIn('category', ['assessment'])->pluck('type')->all()); foreach ($registry as $definition) { $this->assertArrayHasKey('webBehavior', $definition); $this->assertArrayHasKey('exportCompatibility', $definition); $this->postJson($this->blocksUrl($course, $version, $lesson), [ 'type' => $definition['type'], 'schemaVersion' => $definition['schemaVersion'], 'data' => $definition['defaultData'], ])->assertCreated(); } } public function test_block_contract_tokens_persist_and_lock_prevents_mutation_until_unlock(): void { [$designer, $course, $version, $lesson] = $this->foundation(); Sanctum::actingAs($designer); $block = $this->postJson($this->blocksUrl($course, $version, $lesson), [ 'type' => 'quote', 'schemaVersion' => 1, 'data' => ['text' => 'Safe work', 'cite' => 'Team'], 'style' => ['alignment' => 'center', 'width' => 'narrow', 'spacing' => 'l', 'background' => 'accent', 'border' => 'subtle', 'radius' => 'l', 'fontSize' => 18, 'fontWeight' => 600, 'textColor' => '#1f2937', 'lineHeight' => 1.8, 'maxWidth' => 760, 'aspectRatio' => '16/9', 'objectFit' => 'cover'], 'behavior' => ['hidden' => false, 'completion' => 'view', 'animation' => 'fade', 'locked' => true], 'responsive' => ['mobileStack' => true, 'mobileOrder' => 'logical'], 'accessibility' => ['label' => 'Safety quote', 'decorative' => false], ])->assertCreated()->assertJsonPath('data.style.width', 'narrow')->assertJsonPath('data.behavior.locked', true)->json('data'); $this->patchJson("/api/v1/blocks/{$block['id']}", ['expectedRevision' => 1, 'data' => ['text' => 'Changed', 'cite' => 'Team']])->assertUnprocessable()->assertJsonValidationErrors('locked'); $this->putJson("/api/v1/lessons/{$lesson->getKey()}/blocks/order", ['blockIds' => [$block['id']]])->assertUnprocessable()->assertJsonValidationErrors('locked'); $this->patchJson("/api/v1/blocks/{$block['id']}", [ 'expectedRevision' => 1, 'data' => $block['data'], 'style' => $block['style'], 'behavior' => [...$block['behavior'], 'locked' => false], 'responsive' => $block['responsive'], 'accessibility' => $block['accessibility'], ])->assertOk()->assertJsonPath('data.behavior.locked', false); } public function test_autosave_uses_optimistic_revision_control(): void { [$designer, $course, $version, $lesson] = $this->foundation(); Sanctum::actingAs($designer); $blockId = $this->postJson($this->blocksUrl($course, $version, $lesson), [ 'type' => 'text', 'schemaVersion' => 1, 'data' => ['html' => '
One
'], ])->json('data.id'); $this->patchJson("/api/v1/blocks/{$blockId}", [ 'expectedRevision' => 1, 'data' => ['html' => 'Two
'], ])->assertOk()->assertJsonPath('data.revision', 2); $this->patchJson("/api/v1/blocks/{$blockId}", [ 'expectedRevision' => 1, 'data' => ['html' => 'Stale
'], ])->assertConflict()->assertJsonPath('error.code', 'revision_conflict'); } public function test_rich_text_is_sanitized_before_storage_without_losing_supported_formatting(): void { [$designer, $course, $version, $lesson] = $this->foundation(); Sanctum::actingAs($designer); $html = 'Use safe steps.
badgood'; $response = $this->postJson($this->blocksUrl($course, $version, $lesson), [ 'type' => 'text', 'schemaVersion' => 1, 'data' => ['html' => $html], ])->assertCreated(); $stored = $response->json('data.data.html'); $this->assertStringContainsString('