Files
Gnuboard7/tests/Unit/Extension/Helpers/EditorSpecAssemblerTest.php
2026-07-01 10:30:32 +09:00

214 lines
8.0 KiB
PHP

<?php
namespace Tests\Unit\Extension\Helpers;
use App\Extension\Helpers\EditorSpecAssembler;
use Illuminate\Support\Facades\File;
use Tests\TestCase;
/**
* EditorSpecAssembler 단위 테스트.
*
* editor-spec.json 분할(manifest + `$include` 블록) 합본의 정확성·하위 호환·
* 폴백·디그레이드를 검증한다. BASE_PATH 를 건드리지 않고 storage/app 하위 임시
* fixture 디렉토리에 manifest/블록 파일을 구성한 뒤 절대 경로로 합본을 호출한다.
*/
class EditorSpecAssemblerTest extends TestCase
{
private string $testRoot;
protected function setUp(): void
{
parent::setUp();
$this->testRoot = storage_path('app/test_editor_spec_assembler');
File::deleteDirectory($this->testRoot);
File::makeDirectory($this->testRoot, 0755, true);
}
protected function tearDown(): void
{
File::deleteDirectory($this->testRoot);
parent::tearDown();
}
/**
* manifest + `$include` 블록을 top-level merge 로 합본한다.
*/
public function test_assembles_include_blocks_into_single_spec(): void
{
$this->writeJson('editor-spec.json', [
'templateId' => 'demo',
'version' => '1.0.0',
'darkMode' => ['strategy' => 'ancestor-class'],
'$include' => [
'componentPalette' => 'editor-spec/componentPalette.json',
'controls' => 'editor-spec/controls.json',
],
]);
$this->writeJson('editor-spec/componentPalette.json', [
'groups' => [['label' => 'Layout', 'components' => ['Div']]],
'entries' => ['Div' => ['label' => 'Div']],
]);
$this->writeJson('editor-spec/controls.json', [
'textAlign' => ['widget' => 'segmented'],
]);
$spec = EditorSpecAssembler::assemble($this->path('editor-spec.json'));
$this->assertIsArray($spec);
// manifest 인라인 메타 보존
$this->assertSame('demo', $spec['templateId']);
$this->assertSame('1.0.0', $spec['version']);
$this->assertSame(['strategy' => 'ancestor-class'], $spec['darkMode']);
// include 블록이 해당 키 값으로 합본됨
$this->assertSame(['Div' => ['label' => 'Div']], $spec['componentPalette']['entries']);
$this->assertSame(['widget' => 'segmented'], $spec['controls']['textAlign']);
// `$include` 키 자체는 결과에 남지 않음
$this->assertArrayNotHasKey('$include', $spec);
}
/**
* `$include` 없는 미분할 manifest 는 원본 그대로 반환한다(하위 호환).
*/
public function test_returns_manifest_as_is_when_no_include(): void
{
$original = [
'templateId' => 'legacy',
'version' => '1.0.0',
'componentPalette' => ['entries' => ['Div' => ['label' => 'Div']]],
'controls' => ['textAlign' => ['widget' => 'segmented']],
];
$this->writeJson('editor-spec.json', $original);
$spec = EditorSpecAssembler::assemble($this->path('editor-spec.json'));
$this->assertSame($original, $spec);
}
/**
* include 파일 미존재 시 해당 키만 누락하고 나머지는 합본한다(무손실 디그레이드).
*/
public function test_skips_missing_include_block(): void
{
$this->writeJson('editor-spec.json', [
'templateId' => 'demo',
'$include' => [
'controls' => 'editor-spec/controls.json',
'missing' => 'editor-spec/does-not-exist.json',
],
]);
$this->writeJson('editor-spec/controls.json', ['textAlign' => ['widget' => 'segmented']]);
$spec = EditorSpecAssembler::assemble($this->path('editor-spec.json'));
$this->assertIsArray($spec);
$this->assertArrayHasKey('controls', $spec);
$this->assertArrayNotHasKey('missing', $spec);
}
/**
* include 분할본이 manifest 인라인 동일 키를 덮어쓴다(분할본이 정본).
*/
public function test_include_block_overrides_inline_value(): void
{
$this->writeJson('editor-spec.json', [
'controls' => ['textAlign' => ['widget' => 'STALE']],
'$include' => ['controls' => 'editor-spec/controls.json'],
]);
$this->writeJson('editor-spec/controls.json', ['textAlign' => ['widget' => 'segmented']]);
$spec = EditorSpecAssembler::assemble($this->path('editor-spec.json'));
$this->assertSame(['textAlign' => ['widget' => 'segmented']], $spec['controls']);
}
/**
* 합본 결과가 분할 전 단일 파일과 deep-equal 함을 검증한다(분할 무손실 보장).
*/
public function test_assembled_spec_deep_equals_monolithic(): void
{
$monolithic = [
'templateId' => 'demo',
'version' => '1.0.0',
'darkMode' => ['strategy' => 'media-query'],
'componentPalette' => [
'groups' => [['label' => 'L', 'components' => ['Div', 'Span']]],
'entries' => ['Div' => ['label' => 'Div'], 'Span' => ['label' => 'Span']],
],
'controls' => ['textAlign' => ['widget' => 'segmented', 'options' => [1, 2, 3]]],
'sampleData' => ['byDataSourceId' => ['users' => ['data' => [['id' => 1]]]]],
];
// 분할본 구성: 메타+darkMode 인라인, 나머지 블록은 include.
$manifest = [
'templateId' => 'demo',
'version' => '1.0.0',
'darkMode' => ['strategy' => 'media-query'],
'$include' => [
'componentPalette' => 'editor-spec/componentPalette.json',
'controls' => 'editor-spec/controls.json',
'sampleData' => 'editor-spec/sampleData.json',
],
];
$this->writeJson('editor-spec.json', $manifest);
$this->writeJson('editor-spec/componentPalette.json', $monolithic['componentPalette']);
$this->writeJson('editor-spec/controls.json', $monolithic['controls']);
$this->writeJson('editor-spec/sampleData.json', $monolithic['sampleData']);
$spec = EditorSpecAssembler::assemble($this->path('editor-spec.json'));
$this->assertEquals($monolithic, $spec);
}
/**
* 활성 디렉토리에 파일이 없으면 null 을 반환한다(편집 컨트롤 부재 폴백).
*
* _bundled 폴백은 없다 — 활성 경로 단일 기준이므로 미존재 시 즉시 null.
*/
public function test_returns_null_when_active_file_missing(): void
{
// _bundled 에 파일이 있어도 활성 단일 경로만 보므로 무관(폴백 없음).
$this->writeJson('_bundled/editor-spec.json', ['templateId' => 'bundled']);
$spec = EditorSpecAssembler::assemble($this->path('active/editor-spec.json'));
$this->assertNull($spec);
}
/**
* 파싱 실패(비 JSON) manifest 는 null 을 반환한다.
*/
public function test_returns_null_on_invalid_json(): void
{
File::put($this->path('editor-spec.json'), '{ this is not valid json ]');
$spec = EditorSpecAssembler::assemble($this->path('editor-spec.json'));
$this->assertNull($spec);
}
/**
* 테스트 루트 기준 상대 경로에 JSON 을 기록합니다.
*
* @param string $relative testRoot 기준 상대 경로
* @param array<string, mixed> $data 기록할 배열
*/
private function writeJson(string $relative, array $data): void
{
$path = $this->path($relative);
File::ensureDirectoryExists(\dirname($path));
File::put($path, (string) json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
}
/**
* 테스트 루트 기준 상대 경로의 절대 경로를 반환합니다.
*
* @param string $relative testRoot 기준 상대 경로
* @return string 절대 경로
*/
private function path(string $relative): string
{
return $this->testRoot.DIRECTORY_SEPARATOR.str_replace('/', DIRECTORY_SEPARATOR, $relative);
}
}