diff --git a/src/BladeRenderer.php b/src/BladeRenderer.php index 89301e0e..8393e75c 100644 --- a/src/BladeRenderer.php +++ b/src/BladeRenderer.php @@ -39,7 +39,7 @@ public function getTemporaryCachePath(): string /** * Render a Blade template string in isolation by freezing and restoring compiler state. */ - public function render(ComponentNode $component, ComponentSource $source): string + public function render(ComponentNode $component, string $path): string { $temporaryCachePath = $this->getTemporaryCachePath(); @@ -93,17 +93,21 @@ function ($input) { ]); $obLevel = ob_get_level(); - $hash = Utils::hash($source->path); - $path = $temporaryCachePath . '/' . $hash . '.php'; + $hash = Utils::hash($path); + $compiled = $temporaryCachePath . '/' . $hash . '.php'; $fn = '__' . $hash; $this->manager->startFolding(); try { - if (! file_exists($path)) { - $this->blade->compile($source->path); + if (! file_exists($compiled)) { + $this->blade->compile($path); } + $awareData = Arr::mapWithKeys($component->parentsAttributes, function (Attribute $attribute) { + return [$attribute->name => $attribute->getStaticValue()]; + }); + $attributes = Arr::mapWithKeys($component->attributes, function (Attribute $attribute) { return [$attribute->name => $attribute->getStaticValue()]; }); @@ -112,12 +116,13 @@ function ($input) { return [$slot->name => new ComponentSlot($slot->content())]; }); + $this->runtime->pushData($awareData); $this->runtime->pushData($attributes); $this->runtime->pushSlots($slots); ob_start(); - require_once $path; + require_once $compiled; $fn( __blaze: $this->runtime, @@ -131,6 +136,7 @@ function ($input) { ob_end_clean(); } + $this->runtime->popData(); $this->runtime->popData(); $this->manager->stopFolding(); diff --git a/src/Compiler/AwareCompiler.php b/src/Compiler/AwareCompiler.php index adbe30b9..9635b4e4 100644 --- a/src/Compiler/AwareCompiler.php +++ b/src/Compiler/AwareCompiler.php @@ -40,12 +40,6 @@ public function compile(string $expression): string ? sprintf('$%s = $__blaze->getConsumableData(\'%s\', $__awareDefaults[\'%s\']);', $name, $name, $name) : sprintf('$%s = $__blaze->getConsumableData(\'%s\');', $name, $name); - $kebab = Str::kebab($name); - - $output .= $kebab !== $name - ? sprintf(' unset($attributes[\'%s\'], $attributes[\'%s\']);', $name, $kebab) - : sprintf(' unset($attributes[\'%s\']);', $name); - $output .= "\n"; } diff --git a/src/Folder/Foldable.php b/src/Folder/Foldable.php index ec03b8db..533cfbb6 100644 --- a/src/Folder/Foldable.php +++ b/src/Folder/Foldable.php @@ -28,7 +28,7 @@ class Foldable public function __construct( protected ComponentNode $node, - protected ComponentSource $source, + protected string $path, protected BladeRenderer $renderer, protected BladeService $blade, ) { @@ -42,17 +42,13 @@ public function fold(): string $this->renderable = new ComponentNode( name: $this->node->name, prefix: $this->node->prefix, - attributeString: '', - children: [], selfClosing: $this->node->selfClosing, - parentsAttributes: $this->node->parentsAttributes, ); $this->setupAttributes(); $this->setupSlots(); - $this->mergeAwareProps(); - $this->html = $this->renderer->render($this->renderable, $this->source); + $this->html = $this->renderer->render($this->renderable, $this->path); $this->processUncompiledAttributes(); $this->restorePlaceholders(); @@ -62,28 +58,38 @@ public function fold(): string } /** - * Replace dynamic attributes with placeholders, keep static ones as-is. + * Prepare component and inherited attributes for isolated rendering. */ protected function setupAttributes(): void { foreach ($this->node->attributes as $key => $attribute) { - if (! $attribute->isStaticValue()) { - $placeholder = 'BLAZE_PLACEHOLDER_' . $this->placeholderIndex++ . '_'; + $this->renderable->attributes[$key] = $this->prepareAttribute($attribute); + } - $this->attributeByPlaceholder[$placeholder] = $attribute; + foreach ($this->node->parentsAttributes as $key => $attribute) { + $this->renderable->parentsAttributes[$key] = $this->prepareAttribute($attribute); + } + } - $this->renderable->attributes[$key] = new Attribute( - name: $attribute->name, - value: $placeholder, - propName: $attribute->propName, - prefix: '', - dynamic: false, - quotes: '"', - ); - } else { - $this->renderable->attributes[$key] = clone $attribute; - } + /** + * Replace a dynamic attribute with a static placeholder for isolated rendering. + */ + protected function prepareAttribute(Attribute $attribute): Attribute + { + if ($attribute->isStaticValue()) { + return clone $attribute; } + + $placeholder = 'BLAZE_PLACEHOLDER_' . $this->placeholderIndex++ . '_'; + + $this->attributeByPlaceholder[$placeholder] = $attribute; + + return new Attribute( + name: $attribute->name, + value: $placeholder, + propName: $attribute->propName, + dynamic: false, + ); } /** @@ -140,63 +146,6 @@ protected function setupSlots(): void $this->renderable->children = $slots; } - /** - * Merge @aware-declared props from parent attributes into the renderable node. - */ - protected function mergeAwareProps(): void - { - $aware = $this->source->directives->array('aware') ?? []; - - foreach ($aware as $prop => $default) { - if (is_int($prop)) { - $prop = $default; - $default = null; - } - - if (isset($this->renderable->attributes[$prop])) { - continue; - } - - if (isset($this->node->parentsAttributes[$prop])) { - $attribute = $this->node->parentsAttributes[$prop]; - - if (! $attribute->isStaticValue()) { - $placeholder = 'BLAZE_PLACEHOLDER_' . $this->placeholderIndex++ . '_'; - - $this->attributeByPlaceholder[$placeholder] = $attribute; - - $this->renderable->attributes[$prop] = new Attribute( - name: $prop, - value: $placeholder, - propName: $prop, - dynamic: false, - ); - } else { - $this->renderable->attributes[$prop] = new Attribute( - name: $attribute->name, - value: $attribute->value, - propName: $attribute->propName, - dynamic: $attribute->dynamic, - quotes: $attribute->quotes, - prefix: $attribute->prefix, - ); - } - } else if ($default !== null) { - // TODO: test this, we might not need to add the default attributes because they will be added inside the component? - // When the value is null and no parent provides a value, we intentionally - // skip adding the attribute. This lets @aware and @props handle defaults - // at runtime, matching the non-folded behavior. Adding an attribute with - // null value would render as prop="" in HTML, corrupting null to empty string. - $this->renderable->attributes[$prop] = new Attribute( - name: $prop, - value: $default, - propName: $prop, - dynamic: false, - ); - } - } - } - /** * Convert [BLAZE_ATTR:...] markers into conditional PHP for dynamic attributes. */ diff --git a/src/Folder/Folder.php b/src/Folder/Folder.php index 26707e7d..8643bc82 100644 --- a/src/Folder/Folder.php +++ b/src/Folder/Folder.php @@ -60,7 +60,7 @@ public function fold(Node $node): Node $this->checkProblematicPatterns($source); try { - $foldable = new Foldable($node, $source, $this->renderer, $this->blade); + $foldable = new Foldable($node, $source->path, $this->renderer, $this->blade); $html = $foldable->fold(); @@ -103,6 +103,10 @@ protected function isSafeToFold(ComponentSource $source, ComponentNode $node): b return false; } + if (array_key_exists('attributes', $node->parentsAttributes)) { + return false; + } + $dynamicAttributes = array_filter($node->attributes, fn ($attribute) => ! $attribute->isStaticValue()); foreach ($source->directives->aware() as $prop) { diff --git a/tests/BladeRendererTest.php b/tests/BladeRendererTest.php new file mode 100644 index 00000000..c8088933 --- /dev/null +++ b/tests/BladeRendererTest.php @@ -0,0 +1,71 @@ + app(BladeRenderer::class)->deleteTemporaryCacheDirectory()); + +test('compiles component source into the temporary cache', function () { + $path = fixture_path('views/components/foldable/input.blade.php'); + $node = app(Parser::class)->parse('')[0]; + + app(BladeRenderer::class)->render($node, $path); + + expect(File::exists(config('view.compiled').'/blaze/'.Utils::hash($path).'.php'))->toBeTrue(); +}); + +test('makes attributes available to aware props', function () { + $node = app(Parser::class)->parse('')[0]; + + $output = app(BladeRenderer::class)->render($node, fixture_path('views/components/foldable/input-aware.blade.php')); + + expect($output)->toEqualCollapsingWhitespace(''); +}); + +test('makes slots available to aware props', function () { + $node = app(Parser::class)->parse('number')[0]; + + $output = app(BladeRenderer::class)->render($node, fixture_path('views/components/foldable/input-aware.blade.php')); + + expect($output)->toEqualCollapsingWhitespace(''); +}); + +test('makes parents attributes available to aware props', function () { + $node = app(Parser::class)->parse('')[0]; + + $node->setParentsAttributes( + app(AttributeParser::class)->parse('type="number"') + ); + + $output = app(BladeRenderer::class)->render($node, fixture_path('views/components/foldable/input-aware.blade.php')); + + expect($output)->toEqualCollapsingWhitespace(''); +}); + +test('processes unblaze blocks', function () { + $node = app(Parser::class)->parse('')[0]; + + $output = app(BladeRenderer::class)->render($node, fixture_path('views/components/foldable/input-unblaze.blade.php')); + + expect($output)->toEqualCollapsingWhitespace(sprintf('', join('', [ + '', + ' \'address\', ); ?>', + ' {{ $errors->has($scope[\'name\']) }} ', + '', + ]))); +}); + +test('deletes the temporary cache directory', function () { + $node = app(Parser::class)->parse('')[0]; + + app(BladeRenderer::class)->render($node, fixture_path('views/components/foldable/input.blade.php')); + + expect(File::isDirectory(config('view.compiled').'/blaze'))->toBeTrue(); + + app(BladeRenderer::class)->deleteTemporaryCacheDirectory(); + + expect(File::isDirectory(config('view.compiled').'/blaze'))->toBeFalse(); +}); diff --git a/tests/ComparisonTest.php b/tests/ComparisonTest.php index c6ca9934..c9e383d4 100644 --- a/tests/ComparisonTest.php +++ b/tests/ComparisonTest.php @@ -150,3 +150,8 @@ BLADE )); + +test('foldable aware without props', fn () => compare(<<<'BLADE' + + BLADE +)); \ No newline at end of file diff --git a/tests/Compiler/WrapperTest.php b/tests/Compiler/WrapperTest.php index 83c50895..5d3e940f 100644 --- a/tests/Compiler/WrapperTest.php +++ b/tests/Compiler/WrapperTest.php @@ -49,7 +49,7 @@ 'ob_start(); ?> ', '@blaze ', ' \'text\']; ', - '$type = $__blaze->getConsumableData(\'type\', $__awareDefaults[\'type\']); unset($attributes[\'type\']); ', + '$type = $__blaze->getConsumableData(\'type\', $__awareDefaults[\'type\']); ', 'unset($__awareDefaults); ?> ', ' \'text\', \'disabled\' => false]; ', '$type ??= $attributes[\'type\'] ?? $__defaults[\'type\']; unset($attributes[\'type\']); ', diff --git a/tests/Folder/FoldableTest.php b/tests/Folder/FoldableTest.php index c928691c..7def932b 100644 --- a/tests/Folder/FoldableTest.php +++ b/tests/Folder/FoldableTest.php @@ -1,29 +1,210 @@ Artisan::call('view:clear')); +use function Pest\Laravel\mock; -test('folds dynamic attributes', function () { - $input = ''; +test('replaces and restores bound attributes', function () { + $node = app(Parser::class)->parse('')[0]; - $node = app(Parser::class)->parse($input)[0]; - $foldable = new Foldable($node, new ComponentSource(fixture_path('views/components/foldable/input.blade.php')), app(BladeRenderer::class), app(BladeService::class)); + mock(BladeRenderer::class) + ->expects('render') + ->once()->withArgs(function (ComponentNode $node) { + expect($node->render())->toBe(''); + + return true; + }) + ->andReturn(''); + + $output = (new Foldable($node, '', app(BladeRenderer::class), app(BladeService::class)))->fold(); + + expect($output)->toEqualCollapsingWhitespace(''); +}); + +test('preserves bound attributes with static constant values', function (string $value) { + $node = app(Parser::class)->parse('')[0]; + + mock(BladeRenderer::class) + ->expects('render') + ->once()->withArgs(function (ComponentNode $node) use ($value) { + expect($node->render())->toBe(''); + + return true; + }) + ->andReturn(''); + + (new Foldable($node, '', app(BladeRenderer::class), app(BladeService::class)))->fold(); +})->with(['false', 'true', 'null']); + +test('replaces parents attributes', function () { + $node = app(Parser::class)->parse('')[0]; + + $node->setParentsAttributes( + app(AttributeParser::class)->parse(':type="$type"') + ); + + mock(BladeRenderer::class) + ->expects('render') + ->once()->withArgs(function (ComponentNode $node) { + expect($node->render())->toBe(''); + expect($node->parentsAttributes['type']->render())->toBe('type="BLAZE_PLACEHOLDER_0_"'); + + return true; + }) + ->andReturn(''); + + $output = (new Foldable($node, '', app(BladeRenderer::class), app(BladeService::class)))->fold(); + + expect($output)->toEqualCollapsingWhitespace(''); +}); + +test('restores every occurrence of a dynamic attribute placeholder', function () { + $node = app(Parser::class)->parse('')[0]; + + mock(BladeRenderer::class) + ->expects('render') + ->once()->withArgs(function (ComponentNode $node) { + expect($node->render())->toBe(''); + + return true; + }) + ->andReturn(''); + + $output = (new Foldable($node, '', app(BladeRenderer::class), app(BladeService::class)))->fold(); + + expect($output)->toEqualCollapsingWhitespace( + '' + ); +}); + +test('restores bound attributes inside php blocks as raw expressions', function () { + $node = app(Parser::class)->parse('')[0]; + + mock(BladeRenderer::class) + ->expects('render') + ->once()->withArgs(function (ComponentNode $node) { + expect($node->render())->toBe(''); + + return true; + }) + ->andReturn(""); - expect($foldable->fold())->toEqualCollapsingWhitespace( - '' + $output = (new Foldable($node, '', app(BladeRenderer::class), app(BladeService::class)))->fold(); + + expect($output)->toBe(''); +}); + +test('compiles echo attributes restored inside php blocks', function () { + $node = app(Parser::class)->parse('')[0]; + + mock(BladeRenderer::class) + ->expects('render') + ->once()->withArgs(function (ComponentNode $node) { + expect($node->render())->toBe(''); + + return true; + }) + ->andReturn(""); + + $output = (new Foldable($node, '', app(BladeRenderer::class), app(BladeService::class)))->fold(); + + expect($output)->toBe(""); +}); + +test('compiles bound attributes passed through attribute bag', function () { + $node = app(Parser::class)->parse('')[0]; + + mock(BladeRenderer::class) + ->expects('render') + ->once()->withArgs(function (ComponentNode $node) { + expect($node->render())->toBe(''); + + return true; + }) + ->andReturn(''); + + $output = (new Foldable($node, '', app(BladeRenderer::class), app(BladeService::class)))->fold(); + + expect($output)->toEqualCollapsingWhitespace( + sprintf('', join('', [ + '', + 'required=""', + '', + ])) ); }); -test('folds slots', function () { +test('restores unbound attributes passed through attribute bag', function () { + $node = app(Parser::class)->parse('')[0]; + + mock(BladeRenderer::class) + ->expects('render') + ->once()->withArgs(function (ComponentNode $node) { + expect($node->render())->toBe(''); + + return true; + }) + ->andReturn(''); + + $output = (new Foldable($node, '', app(BladeRenderer::class), app(BladeService::class)))->fold(); + + expect($output)->toBe(''); +}); + +test('uses empty strings for true x-data and wire: attributes passed through attribute bag', function (string $attribute) { + $node = app(Parser::class)->parse('')[0]; + + mock(BladeRenderer::class) + ->expects('render') + ->once()->withArgs(function (ComponentNode $node) use ($attribute) { + expect($node->render())->toBe(''); + + return true; + }) + ->andReturn(''); + + $output = (new Foldable($node, '', app(BladeRenderer::class), app(BladeService::class)))->fold(); + + expect($output)->toBe(implode('', [ + '', + $attribute.'=""', + '', + '>', + ])); +})->with(['x-data', 'wire:loading']); + +test('handles newlines consumed by attribute php blocks', function () { + $node = app(Parser::class)->parse('')[0]; + + mock(BladeRenderer::class) + ->expects('render') + ->once()->withArgs(function (ComponentNode $node) { + expect($node->render())->toBe(''); + + return true; + }) + ->andReturn(""); + + $output = (new Foldable($node, '', app(BladeRenderer::class), app(BladeService::class)))->fold(); + + expect($output)->toBe(implode('', [ + "', + 'required=""', + '', + "\n\n>", + ])); +}); + +test('restores named and default slots after rendering', function () { $input = <<<'BLADE' - + Before {{ $title }} @@ -34,13 +215,38 @@ After - BLADE - ; + BLADE; $node = app(Parser::class)->parse($input)[0]; - $foldable = new Foldable($node, new ComponentSource(fixture_path('views/components/foldable/card.blade.php')), app(BladeRenderer::class), app(BladeService::class)); - expect($foldable->fold())->toEqualCollapsingWhitespace(<<<'HTML' + mock(BladeRenderer::class) + ->expects('render') + ->once()->withArgs(function (ComponentNode $node) { + expect($node->render())->toBe(join('', [ + '', + 'BLAZE_PLACEHOLDER_0_', + 'BLAZE_PLACEHOLDER_1_', + 'BLAZE_PLACEHOLDER_2_', + '', + ]) + ); + + return true; + }) + ->andReturn(<<<'HTML' +
+ BLAZE_PLACEHOLDER_0_ +
+ BLAZE_PLACEHOLDER_2_ +
+ BLAZE_PLACEHOLDER_1_ +
+ HTML + ); + + $output = (new Foldable($node, '', app(BladeRenderer::class), app(BladeService::class)))->fold(); + + expect($output)->toEqualCollapsingWhitespace(<<<'HTML'
{{ $title }}
@@ -52,135 +258,147 @@ ); }); -test('preserves dynamic attributes with static false', function () { - $input = ''; +test('does not synthesize a default slot when one is explicit', function () { + $input = <<<'BLADE' + + Ignored loose content + Explicit content + + BLADE; $node = app(Parser::class)->parse($input)[0]; - $foldable = new Foldable($node, new ComponentSource(fixture_path('views/components/foldable/input.blade.php')), app(BladeRenderer::class), app(BladeService::class)); - expect($foldable->fold())->toEqualCollapsingWhitespace( - '' - ); -}); + mock(BladeRenderer::class) + ->expects('render') + ->once()->withArgs(function (ComponentNode $node) { + expect($node->render())->toBe('BLAZE_PLACEHOLDER_0_'); -test('preserves dynamic attributes with static null', function () { - $input = ''; + return true; + }) + ->andReturn('
BLAZE_PLACEHOLDER_0_
'); - $node = app(Parser::class)->parse($input)[0]; - $foldable = new Foldable($node, new ComponentSource(fixture_path('views/components/foldable/input.blade.php')), app(BladeRenderer::class), app(BladeService::class)); + $output = (new Foldable($node, '', app(BladeRenderer::class), app(BladeService::class)))->fold(); - expect($foldable->fold())->toEqualCollapsingWhitespace( - '' - ); + expect($output)->toBe('
Explicit content
'); }); -test('merges aware props from parent attributes', function () { - $input = ''; +test('handles newlines consumed by slot php blocks', function () { + $node = app(Parser::class)->parse('Content')[0]; - $node = app(Parser::class)->parse($input)[0]; - $foldable = new Foldable($node, new ComponentSource(fixture_path('views/components/foldable/input-aware.blade.php')), app(BladeRenderer::class), app(BladeService::class)); - - $node->setParentsAttributes([ - 'type' => new Attribute( - name: 'type', - value: 'number', - propName: 'type', - dynamic: false - ), - ]); - - expect($foldable->fold())->toEqualCollapsingWhitespace( - '' - ); -}); - -test('merges dynamic aware props from parent attributes', function () { - $input = ''; + mock(BladeRenderer::class) + ->expects('render') + ->once()->withArgs(function (ComponentNode $node) { + expect($node->render())->toBe('BLAZE_PLACEHOLDER_0_'); - $node = app(Parser::class)->parse($input)[0]; - $node->setParentsAttributes([ - 'type' => new Attribute( - name: 'type', - value: '$type', - propName: 'type', - dynamic: true, - prefix: ':', - quotes: '"', - ), - ]); - - $foldable = new Foldable($node, new ComponentSource(fixture_path('views/components/foldable/input-aware.blade.php')), app(BladeRenderer::class), app(BladeService::class)); - - expect($foldable->fold())->toEqualCollapsingWhitespace( - '' - ); -}); + return true; + }) + ->andReturn("
BLAZE_PLACEHOLDER_0_\n
"); -test('folds dynamic attributes passed through attribute bag', function () { - $input = ''; + $output = (new Foldable($node, '', app(BladeRenderer::class), app(BladeService::class)))->fold(); - $node = app(Parser::class)->parse($input)[0]; - $foldable = new Foldable($node, new ComponentSource(fixture_path('views/components/foldable/input.blade.php')), app(BladeRenderer::class), app(BladeService::class)); - - expect($foldable->fold())->toEqualCollapsingWhitespace( - sprintf('', join('', [ - '', - 'readonly=""', - '', - ])) - ); -}); - -test('folds dynamic attributes reused under a different key', function () { - $input = ''; - $node = app(Parser::class)->parse($input)[0]; - $foldable = new Foldable($node, new ComponentSource(fixture_path('views/components/foldable/button.blade.php')), app(BladeRenderer::class), app(BladeService::class)); - expect($foldable->fold())->toEqualCollapsingWhitespace( - '' - ); + expect($output)->toBe("
Content\n\n
"); }); test('wraps output with aware macros if descendants use aware', function () { - $input = ''; + $node = app(Parser::class)->parse('')[0]; - $node = app(Parser::class)->parse($input)[0]; $node->hasAwareDescendants = true; - $foldable = new Foldable($node, new ComponentSource(fixture_path('views/components/foldable/wrapper.blade.php')), app(BladeRenderer::class), app(BladeService::class)); + mock(BladeRenderer::class) + ->expects('render') + ->once()->withArgs(function (ComponentNode $node) { + expect($node->render())->toBe(''); + + return true; + }) + ->andReturn('
'); + + $output = (new Foldable($node, '', app(BladeRenderer::class), app(BladeService::class)))->fold(); - expect($foldable->fold())->toEqualCollapsingWhitespace(join('', [ - 'pushData([\'name\' => \'John\']); $__env->pushConsumableComponentData([\'name\' => \'John\']); ?>', - '
', + expect($output)->toEqualCollapsingWhitespace(join('', [ + 'pushData([\'theme\' => \'dark\']); $__env->pushConsumableComponentData([\'theme\' => \'dark\']); ?>', + '
', 'popData(); $__env->popConsumableComponentData(); ?>', ])); }); test('compiles dynamic attributes in aware macros', function () { - $input = ''; + $node = app(Parser::class)->parse('')[0]; - $node = app(Parser::class)->parse($input)[0]; $node->hasAwareDescendants = true; - $foldable = new Foldable($node, new ComponentSource(fixture_path('views/components/foldable/wrapper.blade.php')), app(BladeRenderer::class), app(BladeService::class)); + mock(BladeRenderer::class) + ->expects('render') + ->once()->withArgs(function (ComponentNode $node) { + expect($node->render())->toBe(''); + + return true; + }) + ->andReturn('
'); + + $output = (new Foldable($node, '', app(BladeRenderer::class), app(BladeService::class)))->fold(); - expect($foldable->fold())->toEqualCollapsingWhitespace(join('', [ - 'pushData([\'name\' => $name]); $__env->pushConsumableComponentData([\'name\' => $name]); ?>', - '
', + expect($output)->toEqualCollapsingWhitespace(join('', [ + 'pushData([\'theme\' => $theme]); $__env->pushConsumableComponentData([\'theme\' => $theme]); ?>', + '
', 'popData(); $__env->popConsumableComponentData(); ?>', ])); }); test('compiles echo attributes in aware macros', function () { - $input = ''; - - $node = app(Parser::class)->parse($input)[0]; + $node = app(Parser::class)->parse('')[0]; $node->hasAwareDescendants = true; - $foldable = new Foldable($node, new ComponentSource(fixture_path('views/components/foldable/wrapper.blade.php')), app(BladeRenderer::class), app(BladeService::class)); + mock(BladeRenderer::class) + ->expects('render') + ->once()->withArgs(function (ComponentNode $node) { + expect($node->render())->toBe(''); + + return true; + }) + ->andReturn('
'); - expect($foldable->fold())->toEqualCollapsingWhitespace(join('', [ - 'pushData([\'name\' => \'Mr. \'.e($name)]); $__env->pushConsumableComponentData([\'name\' => \'Mr. \'.e($name)]); ?>', - '
', + $output = (new Foldable($node, '', app(BladeRenderer::class), app(BladeService::class)))->fold(); + + expect($output)->toEqualCollapsingWhitespace(join('', [ + 'pushData([\'theme\' => \'dark-\'.e($variant)]); $__env->pushConsumableComponentData([\'theme\' => \'dark-\'.e($variant)]); ?>', + '
', 'popData(); $__env->popConsumableComponentData(); ?>', ])); }); + +test('does not add aware macros to components without attributes', function () { + $node = app(Parser::class)->parse('')[0]; + $node->hasAwareDescendants = true; + + mock(BladeRenderer::class) + ->expects('render') + ->once()->withArgs(function (ComponentNode $node) { + expect($node->render())->toBe(''); + + return true; + }) + ->andReturn('
'); + + expect((new Foldable($node, '', app(BladeRenderer::class), app(BladeService::class)))->fold()) + ->toBe('
'); +}); + +test('does not add aware macros for inherited attributes only', function () { + $node = app(Parser::class)->parse('')[0]; + $node->hasAwareDescendants = true; + $node->setParentsAttributes(app(AttributeParser::class)->parse('theme="dark"')); + + mock(BladeRenderer::class) + ->expects('render') + ->once()->withArgs(function (ComponentNode $node) { + expect($node->render())->toBe(''); + expect($node->parentsAttributes['theme']->render())->toBe('theme="dark"'); + + return true; + }) + ->andReturn('
'); + + expect((new Foldable($node, '', app(BladeRenderer::class), app(BladeService::class)))->fold()) + ->toBe('
'); +}); diff --git a/tests/Folder/FolderTest.php b/tests/Folder/FolderTest.php index 45f70d24..c0a0889b 100644 --- a/tests/Folder/FolderTest.php +++ b/tests/Folder/FolderTest.php @@ -80,6 +80,20 @@ expect($folded)->toBeInstanceOf(ComponentNode::class); }); +test('does not fold components with attribute spread from parent', function () { + $input = ''; + + $node = app(Parser::class)->parse($input)[0]; + + $node->setParentsAttributes( + app(AttributeParser::class)->parse(':attributes="$attributes"') + ); + + $folded = app(Folder::class)->fold($node); + + expect($folded)->toBeInstanceOf(ComponentNode::class); +}); + test('folds components with slots', function () { $input = 'HeaderBody'; diff --git a/tests/Folder/UnblazeTest.php b/tests/Folder/UnblazeTest.php index 508548e4..bdc7c907 100644 --- a/tests/Folder/UnblazeTest.php +++ b/tests/Folder/UnblazeTest.php @@ -3,14 +3,13 @@ use Livewire\Blaze\BladeRenderer; use Livewire\Blaze\BladeService; use Livewire\Blaze\Folder\Foldable; -use Livewire\Blaze\Support\ComponentSource; use Livewire\Blaze\Parser\Parser; test('compiles unblaze blocks', function () { $input = ''; $node = app(Parser::class)->parse($input)[0]; - $foldable = new Foldable($node, new ComponentSource(fixture_path('views/components/foldable/input-unblaze.blade.php')), app(BladeRenderer::class), app(BladeService::class)); + $foldable = new Foldable($node, fixture_path('views/components/foldable/input-unblaze.blade.php'), app(BladeRenderer::class), app(BladeService::class)); expect($foldable->fold())->toEqualCollapsingWhitespace( sprintf('', join('', [ @@ -26,7 +25,7 @@ $input = ''; $node = app(Parser::class)->parse($input)[0]; - $foldable = new Foldable($node, new ComponentSource(fixture_path('views/components/foldable/nested-input-unblaze.blade.php')), app(BladeRenderer::class), app(BladeService::class)); + $foldable = new Foldable($node, fixture_path('views/components/foldable/nested-input-unblaze.blade.php'), app(BladeRenderer::class), app(BladeService::class)); expect($foldable->fold())->toEqualCollapsingWhitespace( sprintf('
', join('', [ @@ -42,7 +41,7 @@ $input = ''; $node = app(Parser::class)->parse($input)[0]; - $foldable = new Foldable($node, new ComponentSource(fixture_path('views/components/foldable/input-unblaze.blade.php')), app(BladeRenderer::class), app(BladeService::class)); + $foldable = new Foldable($node, fixture_path('views/components/foldable/input-unblaze.blade.php'), app(BladeRenderer::class), app(BladeService::class)); expect($foldable->fold())->toEqualCollapsingWhitespace( sprintf('', join('', [ diff --git a/tests/fixtures/views/components/foldable/input-aware-no-props.blade.php b/tests/fixtures/views/components/foldable/input-aware-no-props.blade.php new file mode 100644 index 00000000..cc7158ba --- /dev/null +++ b/tests/fixtures/views/components/foldable/input-aware-no-props.blade.php @@ -0,0 +1,8 @@ +@blaze(fold: true) + +@aware(['type']) + +{{-- We intentionally do not set @props here to ensure aware props are preserved in $attributes --}} +{{-- When type="number" is passed, below should be rendered as --}} + + diff --git a/tests/fixtures/views/components/foldable/input-aware.blade.php b/tests/fixtures/views/components/foldable/input-aware.blade.php index ddc6e6c0..626f42d7 100644 --- a/tests/fixtures/views/components/foldable/input-aware.blade.php +++ b/tests/fixtures/views/components/foldable/input-aware.blade.php @@ -1,5 +1,7 @@ @blaze(fold: true) -@aware(['type' => 'text']) +@aware(['type']) - +@props(['type' => 'text']) + +