Skip to content

Commit f72e976

Browse files
committed
test(visual-editor): Add unit tests for core functionality
Add comprehensive test suite using Pest PHP: Tests: - BlockRegistryTest: Registration, retrieval, grouping, validation - BlockRendererTest: Rendering, styling, sanitization, recursion - BlockTypesTest: All 10 block types (layout, basic, utility) - VisualLayoutTest: Model CRUD, versions, soft deletes Test infrastructure: - TestCase.php: Base test class with helpers - Pest.php: Configuration and custom expectations - phpunit.xml: PHPUnit configuration Coverage includes: - Block registration and retrieval - Layout rendering with nested blocks - XSS protection and HTML sanitization - Block prop validation - Model relationships and casting
1 parent 95d7e96 commit f72e976

6 files changed

Lines changed: 1205 additions & 0 deletions

File tree

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
3+
use SolutionForest\InspireCmsVisualEditor\Tests\TestCase;
4+
5+
/*
6+
|--------------------------------------------------------------------------
7+
| Test Case
8+
|--------------------------------------------------------------------------
9+
*/
10+
11+
uses(TestCase::class)->in('Unit', 'Feature');
12+
13+
/*
14+
|--------------------------------------------------------------------------
15+
| Expectations
16+
|--------------------------------------------------------------------------
17+
*/
18+
19+
expect()->extend('toBeValidBlock', function () {
20+
return $this
21+
->toBeArray()
22+
->toHaveKeys(['id', 'type']);
23+
});
24+
25+
expect()->extend('toBeValidLayout', function () {
26+
return $this
27+
->toBeArray()
28+
->toHaveKey('type')
29+
->and($this->value['type'])->toBe('container');
30+
});
31+
32+
/*
33+
|--------------------------------------------------------------------------
34+
| Functions
35+
|--------------------------------------------------------------------------
36+
*/
37+
38+
function createBlock(string $type, array $settings = []): array
39+
{
40+
return [
41+
'id' => 'block_' . uniqid(),
42+
'type' => $type,
43+
'settings' => $settings,
44+
'styles' => [],
45+
'children' => [],
46+
];
47+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
<?php
2+
3+
namespace SolutionForest\InspireCmsVisualEditor\Tests;
4+
5+
use Illuminate\Foundation\Testing\RefreshDatabase;
6+
use Orchestra\Testbench\TestCase as Orchestra;
7+
use SolutionForest\InspireCmsVisualEditor\VisualEditorServiceProvider;
8+
9+
class TestCase extends Orchestra
10+
{
11+
use RefreshDatabase;
12+
13+
protected function setUp(): void
14+
{
15+
parent::setUp();
16+
17+
// Run migrations
18+
$this->loadMigrationsFrom(__DIR__ . '/../database/migrations');
19+
}
20+
21+
protected function getPackageProviders($app): array
22+
{
23+
return [
24+
VisualEditorServiceProvider::class,
25+
];
26+
}
27+
28+
protected function getEnvironmentSetUp($app): void
29+
{
30+
// Set up test database
31+
$app['config']->set('database.default', 'testing');
32+
$app['config']->set('database.connections.testing', [
33+
'driver' => 'sqlite',
34+
'database' => ':memory:',
35+
'prefix' => '',
36+
]);
37+
38+
// Visual editor config
39+
$app['config']->set('visual-editor.table_prefix', 'test_');
40+
$app['config']->set('visual-editor.ai.provider', 'anthropic');
41+
}
42+
43+
/**
44+
* Create a sample block data structure
45+
*/
46+
protected function createSampleBlock(string $type, array $settings = [], array $children = []): array
47+
{
48+
return [
49+
'id' => 'block_' . uniqid(),
50+
'type' => $type,
51+
'settings' => $settings,
52+
'styles' => [],
53+
'children' => $children,
54+
];
55+
}
56+
57+
/**
58+
* Create a sample layout structure
59+
*/
60+
protected function createSampleLayout(): array
61+
{
62+
return [
63+
'id' => 'block_root',
64+
'type' => 'container',
65+
'settings' => ['maxWidth' => '1200px'],
66+
'styles' => [],
67+
'children' => [
68+
[
69+
'id' => 'block_section1',
70+
'type' => 'section',
71+
'settings' => ['contentWidth' => 'boxed'],
72+
'styles' => ['padding' => '2rem'],
73+
'children' => [
74+
[
75+
'id' => 'block_heading1',
76+
'type' => 'heading',
77+
'settings' => ['content' => 'Hello World', 'level' => 1],
78+
'styles' => [],
79+
'children' => [],
80+
],
81+
[
82+
'id' => 'block_text1',
83+
'type' => 'text',
84+
'settings' => ['content' => '<p>This is sample text content.</p>'],
85+
'styles' => [],
86+
'children' => [],
87+
],
88+
],
89+
],
90+
],
91+
];
92+
}
93+
}
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
<?php
2+
3+
use SolutionForest\InspireCmsVisualEditor\Blocks\Registry\BlockRegistry;
4+
use SolutionForest\InspireCmsVisualEditor\Blocks\Types\ContainerBlock;
5+
use SolutionForest\InspireCmsVisualEditor\Blocks\Types\HeadingBlock;
6+
use SolutionForest\InspireCmsVisualEditor\Blocks\Types\TextBlock;
7+
use SolutionForest\InspireCmsVisualEditor\Enums\BlockCategory;
8+
9+
beforeEach(function () {
10+
BlockRegistry::clear();
11+
});
12+
13+
describe('BlockRegistry', function () {
14+
it('can register a block', function () {
15+
BlockRegistry::register(HeadingBlock::class);
16+
17+
expect(BlockRegistry::has('heading'))->toBeTrue();
18+
});
19+
20+
it('can register multiple blocks', function () {
21+
BlockRegistry::registerMany([
22+
ContainerBlock::class,
23+
HeadingBlock::class,
24+
TextBlock::class,
25+
]);
26+
27+
expect(BlockRegistry::has('container'))->toBeTrue();
28+
expect(BlockRegistry::has('heading'))->toBeTrue();
29+
expect(BlockRegistry::has('text'))->toBeTrue();
30+
});
31+
32+
it('can get a registered block by type', function () {
33+
BlockRegistry::register(HeadingBlock::class);
34+
35+
$block = BlockRegistry::get('heading');
36+
37+
expect($block)->toBeInstanceOf(HeadingBlock::class);
38+
expect($block->getType())->toBe('heading');
39+
});
40+
41+
it('returns null for unregistered block type', function () {
42+
expect(BlockRegistry::get('nonexistent'))->toBeNull();
43+
});
44+
45+
it('can get all registered blocks', function () {
46+
BlockRegistry::registerMany([
47+
ContainerBlock::class,
48+
HeadingBlock::class,
49+
]);
50+
51+
$blocks = BlockRegistry::all();
52+
53+
expect($blocks)->toHaveCount(2);
54+
expect($blocks->keys()->toArray())->toContain('container', 'heading');
55+
});
56+
57+
it('can get blocks by category', function () {
58+
BlockRegistry::registerMany([
59+
ContainerBlock::class,
60+
HeadingBlock::class,
61+
TextBlock::class,
62+
]);
63+
64+
$layoutBlocks = BlockRegistry::byCategory(BlockCategory::Layout);
65+
$basicBlocks = BlockRegistry::byCategory(BlockCategory::Basic);
66+
67+
expect($layoutBlocks)->toHaveCount(1);
68+
expect($basicBlocks)->toHaveCount(2);
69+
});
70+
71+
it('can get blocks grouped by category', function () {
72+
BlockRegistry::registerMany([
73+
ContainerBlock::class,
74+
HeadingBlock::class,
75+
TextBlock::class,
76+
]);
77+
78+
$grouped = BlockRegistry::groupedByCategory();
79+
80+
expect($grouped)->toHaveKey('layout');
81+
expect($grouped)->toHaveKey('basic');
82+
});
83+
84+
it('can get container blocks only', function () {
85+
BlockRegistry::registerMany([
86+
ContainerBlock::class,
87+
HeadingBlock::class,
88+
]);
89+
90+
$containers = BlockRegistry::containers();
91+
92+
expect($containers)->toHaveCount(1);
93+
expect($containers->first()->getType())->toBe('container');
94+
});
95+
96+
it('can clear all registered blocks', function () {
97+
BlockRegistry::register(HeadingBlock::class);
98+
expect(BlockRegistry::has('heading'))->toBeTrue();
99+
100+
BlockRegistry::clear();
101+
102+
expect(BlockRegistry::has('heading'))->toBeFalse();
103+
expect(BlockRegistry::all())->toBeEmpty();
104+
});
105+
106+
it('can create block data with default props', function () {
107+
BlockRegistry::register(HeadingBlock::class);
108+
109+
$blockData = BlockRegistry::createBlockData('heading');
110+
111+
expect($blockData)->toBeArray();
112+
expect($blockData)->toHaveKeys(['id', 'type', 'props', 'styles', 'children']);
113+
expect($blockData['type'])->toBe('heading');
114+
expect($blockData['id'])->toStartWith('block_');
115+
});
116+
117+
it('can create block data with custom id', function () {
118+
BlockRegistry::register(HeadingBlock::class);
119+
120+
$blockData = BlockRegistry::createBlockData('heading', 'my_custom_id');
121+
122+
expect($blockData['id'])->toBe('my_custom_id');
123+
});
124+
125+
it('returns null when creating data for unregistered block', function () {
126+
expect(BlockRegistry::createBlockData('nonexistent'))->toBeNull();
127+
});
128+
129+
it('can get blocks for panel display', function () {
130+
BlockRegistry::registerMany([
131+
ContainerBlock::class,
132+
HeadingBlock::class,
133+
]);
134+
135+
$panelBlocks = BlockRegistry::getBlocksForPanel();
136+
137+
expect($panelBlocks)->toBeArray();
138+
expect($panelBlocks[0])->toHaveKeys(['key', 'label', 'icon', 'blocks']);
139+
});
140+
141+
it('generates unique block IDs', function () {
142+
$id1 = BlockRegistry::generateBlockId();
143+
$id2 = BlockRegistry::generateBlockId();
144+
145+
expect($id1)->toStartWith('block_');
146+
expect($id2)->toStartWith('block_');
147+
expect($id1)->not->toBe($id2);
148+
});
149+
});
150+
151+
describe('BlockRegistry validation', function () {
152+
beforeEach(function () {
153+
BlockRegistry::registerMany([
154+
ContainerBlock::class,
155+
HeadingBlock::class,
156+
TextBlock::class,
157+
]);
158+
});
159+
160+
it('validates a valid layout structure', function () {
161+
$layout = [
162+
'root' => [
163+
'id' => 'block_1',
164+
'type' => 'container',
165+
'props' => [],
166+
'children' => [
167+
[
168+
'id' => 'block_2',
169+
'type' => 'heading',
170+
'props' => ['content' => 'Hello'],
171+
'children' => [],
172+
],
173+
],
174+
],
175+
];
176+
177+
$errors = BlockRegistry::validateLayout($layout);
178+
179+
expect($errors)->toBeEmpty();
180+
});
181+
182+
it('returns error for layout without root', function () {
183+
$layout = [];
184+
185+
$errors = BlockRegistry::validateLayout($layout);
186+
187+
expect($errors)->toContain('Layout must have a root element');
188+
});
189+
190+
it('returns error for block without type', function () {
191+
$layout = [
192+
'root' => [
193+
'id' => 'block_1',
194+
'children' => [],
195+
],
196+
];
197+
198+
$errors = BlockRegistry::validateLayout($layout);
199+
200+
expect($errors)->not->toBeEmpty();
201+
});
202+
203+
it('returns error for unknown block type', function () {
204+
$layout = [
205+
'root' => [
206+
'id' => 'block_1',
207+
'type' => 'unknown_type',
208+
'children' => [],
209+
],
210+
];
211+
212+
$errors = BlockRegistry::validateLayout($layout);
213+
214+
expect($errors)->toContain("Unknown block type 'unknown_type' at root");
215+
});
216+
});

0 commit comments

Comments
 (0)