diff --git a/src/Entity/JavaScriptComponent.php b/src/Entity/JavaScriptComponent.php index e9d72dbd1d9e35442520fac2de8a938ba2349411..326508d03fb44ad300e647171475f1994570b86a 100644 --- a/src/Entity/JavaScriptComponent.php +++ b/src/Entity/JavaScriptComponent.php @@ -168,9 +168,9 @@ final class JavaScriptComponent extends ConfigEntityBase implements XbAssetInter ]); } + $violation_list = new EntityConstraintViolationList($this); if (array_key_exists('source_code_js', $data) || array_key_exists('compiled_js', $data)) { if (!array_key_exists('imported_js_components', $data)) { - $violation_list = new EntityConstraintViolationList($this); $violation_list->add(new ConstraintViolation( "The 'imported_js_components' field is required when 'source_code_js' or 'compiled_js' is provided", "The 'imported_js_components' field is required when 'source_code_js' or 'compiled_js' is provided", @@ -181,6 +181,22 @@ final class JavaScriptComponent extends ConfigEntityBase implements XbAssetInter )); throw new ConstraintViolationException($violation_list); } + foreach ($data['imported_js_components'] as $key => $js_component_name) { + // Test that the imported_js_components are valid names. + if (!preg_match('/^[a-z0-9_-]+$/', $js_component_name)) { + $violation_list->add(new ConstraintViolation( + "The 'imported_js_components' contains an invalid component name.", + "The 'imported_js_components' contains an invalid component name.", + [], + NULL, + "imported_js_components", + NULL + )); + } + } + if ($violation_list->count() > 0) { + throw new ConstraintViolationException($violation_list); + } // The client calculates imported JavaScript components dependencies. This // value is never returned to the client as it will always recalculate it // based off source_code_js. @@ -358,8 +374,8 @@ final class JavaScriptComponent extends ConfigEntityBase implements XbAssetInter $js_component = JavaScriptComponent::load($js_component_name); if (!$js_component) { $violation_list->add(new ConstraintViolation( - "The JavaScript component with machine name '$js_component_name' does not exist.", - "The JavaScript component with machine name '$js_component_name' does not exist.", + "The JavaScript component with the machine name '$js_component_name' does not exist.", + "The JavaScript component with the machine name '$js_component_name' does not exist.", [], NULL, "imported_js_components.$key", @@ -437,4 +453,12 @@ final class JavaScriptComponent extends ConfigEntityBase implements XbAssetInter return self::loadMultiple($js_component_ids); } + public function getCacheTags() { + $cache_tags = parent::getCacheTags(); + if ($dependencies = $this->getDependencies()) { + $cache_tags = array_merge($cache_tags, array_map(fn($dependency) => "config:$dependency", $dependencies['config'] ?? [])); + } + return $cache_tags; + } + } diff --git a/src/Plugin/ExperienceBuilder/ComponentSource/JsComponent.php b/src/Plugin/ExperienceBuilder/ComponentSource/JsComponent.php index d624af72cbfe554ac320b4684512592af8a930dd..92ce9e429047e88fc2df382e4c2bf1a94867ef21 100644 --- a/src/Plugin/ExperienceBuilder/ComponentSource/JsComponent.php +++ b/src/Plugin/ExperienceBuilder/ComponentSource/JsComponent.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Drupal\experience_builder\Plugin\ExperienceBuilder\ComponentSource; +use Drupal\Core\Cache\CacheableMetadata; use Drupal\Core\Config\Entity\ConfigEntityStorageInterface; use Drupal\Core\Extension\ExtensionPathResolver; use Drupal\Core\File\FileUrlGeneratorInterface; @@ -165,7 +166,16 @@ final class JsComponent extends GeneratedFieldExplicitInputUxComponentSourceBase \assert($autoSave->data !== NULL); $component = $component->forAutoSavePreview($autoSave->data); } + if ($isPreview) { + $build['#cache']['tags'][] = AutoSaveManager::CACHE_TAG; + } + $valid_props = $component->getProps() ?? []; + + CacheableMetadata::createFromRenderArray($build) + ->addCacheableDependency($component) + ->applyTo($build); + return $build + [ '#type' => 'astro_island', '#uuid' => $componentUuid, diff --git a/tests/modules/xb_test_code_components/config/install/experience_builder.component.js.xb_test_code_components_using_imports.yml b/tests/modules/xb_test_code_components/config/install/experience_builder.component.js.xb_test_code_components_using_imports.yml new file mode 100644 index 0000000000000000000000000000000000000000..185f06f147f7c51da66013c7875feef4d06b661f --- /dev/null +++ b/tests/modules/xb_test_code_components/config/install/experience_builder.component.js.xb_test_code_components_using_imports.yml @@ -0,0 +1,14 @@ +uuid: dd1e8922-118a-4ff7-b83c-bc8543efdc5a +langcode: en +status: true +dependencies: + config: + - experience_builder.js_component.xb_test_code_components_using_imports +label: 'Component using imports' +id: js.xb_test_code_components_using_imports +provider: null +source: js +category: '@todo' +settings: + local_source_id: xb_test_code_components_using_imports + prop_field_definitions: { } diff --git a/tests/modules/xb_test_code_components/config/install/experience_builder.js_component.xb_test_code_components_using_imports.yml b/tests/modules/xb_test_code_components/config/install/experience_builder.js_component.xb_test_code_components_using_imports.yml new file mode 100644 index 0000000000000000000000000000000000000000..66c15c7aa9fb67e5e9fb174fe9c762c932289b0b --- /dev/null +++ b/tests/modules/xb_test_code_components/config/install/experience_builder.js_component.xb_test_code_components_using_imports.yml @@ -0,0 +1,88 @@ +uuid: 82d79d93-c4b8-4413-9b76-f89ffe432bc1 +langcode: en +status: true +dependencies: + enforced: + config: + - experience_builder.js_component.xb_test_code_components_with_no_props + - experience_builder.js_component.xb_test_code_components_with_props +machineName: xb_test_code_components_using_imports +name: 'Component using imports' +block_override: null +required: { } +props: { } +slots: { } +js: + original: | + import WithNoProps from '@/components/xb_test_code_components_with_no_props'; + import WithProps from '@/components/xb_test_code_components_with_props'; + import FormattedText from "@/lib/FormattedText"; + import { cn } from "@/lib/utils"; + + const UsingImports = ({ + title = "<h3>Component using imports</h3>", + }) => { + return ( + <div className="test-component-using-specific-imports"> + <h2>This component imports specific components</h2> + <div className="imported-components"> + <div className="no-props-component"> + <h3>Component with no props:</h3> + <WithNoProps /> + </div> + <div className="props-component"> + <h3>Component with props:</h3> + <WithProps name="Imported Name" age={25} /> + </div> + </div> + </div> + ); + }; + + export default UsingImports; + compiled: | + import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; + import WithNoProps from '@/components/xb_test_code_components_with_no_props'; + import WithProps from '@/components/xb_test_code_components_with_props'; + import FormattedText from "@/lib/FormattedText"; + import { cn } from "@/lib/utils"; + const UsingImports = ({ title = "<h3>Component using imports</h3>" })=>{ + return /*#__PURE__*/ _jsxs("div", { + className: "test-component-using-specific-imports", + children: [ + /*#__PURE__*/ _jsx("h2", { + children: "This component imports specific components" + }), + /*#__PURE__*/ _jsxs("div", { + className: "imported-components", + children: [ + /*#__PURE__*/ _jsxs("div", { + className: "no-props-component", + children: [ + /*#__PURE__*/ _jsx("h3", { + children: "Component with no props:" + }), + /*#__PURE__*/ _jsx(WithNoProps, {}) + ] + }), + /*#__PURE__*/ _jsxs("div", { + className: "props-component", + children: [ + /*#__PURE__*/ _jsx("h3", { + children: "Component with props:" + }), + /*#__PURE__*/ _jsx(WithProps, { + name: "Imported Name", + age: 25 + }) + ] + }) + ] + }) + ] + }); + }; + export default UsingImports; +css: + original: '' + compiled: '' diff --git a/tests/src/Functional/PropSourceEndpointTest.php b/tests/src/Functional/PropSourceEndpointTest.php index 9cdc6a55efa10e6c86c65d2504281066355194aa..f9ac34dd7e52482f4c4589afd3b340f35b103d05 100644 --- a/tests/src/Functional/PropSourceEndpointTest.php +++ b/tests/src/Functional/PropSourceEndpointTest.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Drupal\Tests\experience_builder\Functional; use Drupal\Component\Serialization\Json; +use Drupal\experience_builder\AutoSave\AutoSaveManager; use Drupal\experience_builder\Entity\Component; use Drupal\node\Entity\Node; use Drupal\Tests\system\Functional\Cache\AssertPageCacheContextsAndTagsTrait; @@ -60,6 +61,7 @@ class PropSourceEndpointTest extends FunctionalTestBase { 'comment_list', 'config:component_list', 'config:core.extension', + 'config:experience_builder.js_component.my-cta', 'config:search.settings', 'config:system.menu.account', 'config:system.menu.admin', @@ -77,6 +79,7 @@ class PropSourceEndpointTest extends FunctionalTestBase { 'user:0', 'user:1', 'user_list', + AutoSaveManager::CACHE_TAG, ]; $expected_contexts = [ diff --git a/tests/src/Functional/XbConfigEntityHttpApiTest.php b/tests/src/Functional/XbConfigEntityHttpApiTest.php index b6695464103d0646b402d27b072426a32762602f..dde73fb6f69ae8834d244efc91273d6045f295a6 100644 --- a/tests/src/Functional/XbConfigEntityHttpApiTest.php +++ b/tests/src/Functional/XbConfigEntityHttpApiTest.php @@ -4,9 +4,10 @@ declare(strict_types=1); namespace Drupal\Tests\experience_builder\Functional; +use Drupal\Core\Cache\Cache; +use Drupal\Core\Url; use Drupal\experience_builder\Audit\ComponentAudit; use Drupal\experience_builder\AutoSave\AutoSaveManager; -use Drupal\Core\Url; use Drupal\experience_builder\Entity\AssetLibrary; use Drupal\experience_builder\Entity\Component; use Drupal\experience_builder\Entity\ComponentInterface; @@ -583,7 +584,7 @@ class XbConfigEntityHttpApiTest extends HttpApiTestBase { $this->assertSame([ 'errors' => [ [ - 'detail' => "The JavaScript component with machine name 'missing' does not exist.", + 'detail' => "The JavaScript component with the machine name 'missing' does not exist.", 'source' => ['pointer' => 'imported_js_components.0'], ], ], @@ -873,8 +874,16 @@ class XbConfigEntityHttpApiTest extends HttpApiTestBase { // @see docs/config-management.md#3.2.1 $this->assertNotNull(Component::load('js.test')); $this->assertTrue(Component::load('js.test')->status()); - $this->assertExposedCodeComponents(['js.test'], 'MISS', $request_options); - $this->assertExposedCodeComponents(['js.test'], 'HIT', $request_options); + $this->assertExposedCodeComponents(['js.test'], 'MISS', $request_options, [ + AutoSaveManager::CACHE_TAG, + 'config:experience_builder.js_component.another_component', + 'config:experience_builder.js_component.test', + ]); + $this->assertExposedCodeComponents(['js.test'], 'HIT', $request_options, [ + AutoSaveManager::CACHE_TAG, + 'config:experience_builder.js_component.another_component', + 'config:experience_builder.js_component.test', + ]); // Confirm that there STILL is an auto-save, and its `status` was updated! $auto_save_data['status'] = TRUE; $this->assertCurrentAutoSave(200, $auto_save_data, JavaScriptComponent::ENTITY_TYPE_ID, 'test'); @@ -1090,7 +1099,7 @@ class XbConfigEntityHttpApiTest extends HttpApiTestBase { ], json_decode((string) $response->getBody(), TRUE)); } - private function assertExposedCodeComponents(array $expected, string $expected_dynamic_page_cache, array $request_options): void { + private function assertExposedCodeComponents(array $expected, string $expected_dynamic_page_cache, array $request_options, array $additional_expected_cache_tags = []): void { assert(in_array($expected_dynamic_page_cache, ['HIT', 'MISS'], TRUE)); $expected_contexts = [ 'languages:language_content', @@ -1107,7 +1116,7 @@ class XbConfigEntityHttpApiTest extends HttpApiTestBase { // @see \Drupal\experience_builder\Controller\ApiComponentsController::getCacheableClientSideInfo() 'user.roles:anonymous', ]; - $body = $this->assertExpectedResponse('GET', Url::fromUri('base:/xb/api/v0/config/component'), $request_options, 200, $expected_contexts, [ + $expected_cache_tags = [ 'CACHE_MISS_IF_UNCACHEABLE_HTTP_METHOD:form', 'config:component_list', 'config:core.extension', @@ -1127,7 +1136,11 @@ class XbConfigEntityHttpApiTest extends HttpApiTestBase { 'user:1', 'user:2', 'user_list', - ], 'UNCACHEABLE (request policy)', $expected_dynamic_page_cache); + ]; + // If expected adds new components, those components add additional cache tags. If those cache tags are not + // present, the test will fail. This array is used to add those additional expected cache tags. + $expected_cache_tags = Cache::mergeTags($expected_cache_tags, $additional_expected_cache_tags); + $body = $this->assertExpectedResponse('GET', Url::fromUri('base:/xb/api/v0/config/component'), $request_options, 200, $expected_contexts, $expected_cache_tags, 'UNCACHEABLE (request policy)', $expected_dynamic_page_cache); self:self::assertNotNull($body); $component_config_entity_ids = array_keys($body); self::assertSame( diff --git a/tests/src/Kernel/Config/JavaScriptComponentValidationTest.php b/tests/src/Kernel/Config/JavaScriptComponentValidationTest.php index 71cb9fd3bd4fd38b1e0b7cdb4cd7d0ac8e87cf18..3a27df11d23fd0011139bbe2a75b010d0d102b52 100644 --- a/tests/src/Kernel/Config/JavaScriptComponentValidationTest.php +++ b/tests/src/Kernel/Config/JavaScriptComponentValidationTest.php @@ -4,9 +4,11 @@ declare(strict_types=1); namespace Drupal\Tests\experience_builder\Kernel\Config; -// cspell:ignore sofie +// cspell:ignore sofie componente extraño use Drupal\experience_builder\Entity\JavaScriptComponent; +use Drupal\experience_builder\Exception\ConstraintViolationException; +use Drupal\Tests\experience_builder\Traits\BetterConfigDependencyManagerTrait; /** * Tests validation of JavaScriptComponent entities. @@ -17,6 +19,8 @@ use Drupal\experience_builder\Entity\JavaScriptComponent; */ class JavaScriptComponentValidationTest extends BetterConfigEntityValidationTestBase { + use BetterConfigDependencyManagerTrait; + /** * {@inheritdoc} */ @@ -56,9 +60,7 @@ class JavaScriptComponentValidationTest extends BetterConfigEntityValidationTest */ protected function setUp(): void { parent::setUp(); - - $this->entity = JavaScriptComponent::create([ - 'machineName' => 'test', + $javascript_component_base = [ 'name' => 'Test', 'status' => TRUE, 'props' => [ @@ -86,10 +88,48 @@ class JavaScriptComponentValidationTest extends BetterConfigEntityValidationTest 'original' => '.test { display: none; }', 'compiled' => '.test{display:none;}', ], + ]; + JavaScriptComponent::create([...$javascript_component_base, 'machineName' => 'other'])->save(); + $this->entity = JavaScriptComponent::create([ + ...$javascript_component_base, + 'machineName' => 'test', + 'dependencies' => [ + 'enforced' => [ + 'config' => [ + // @phpstan-ignore-next-line + JavaScriptComponent::load('other')->getConfigDependencyName(), + ], + ], + ], ]); $this->entity->save(); } + /** + * {@inheritdoc} + */ + public function testEntityIsValid(): void { + parent::testEntityIsValid(); + + // Beyond validity, validate config dependencies are computed correctly. + $this->assertSame( + [ + 'config' => [ + 'experience_builder.js_component.other', + ], + ], + $this->entity->getDependencies() + ); + $this->assertSame([ + 'config' => [ + 'experience_builder.js_component.other', + ], + 'module' => [ + 'experience_builder', + ], + ], $this->getAllDependencies($this->entity)); + } + /** * @testWith [true, true, []] * [true, false, {"": "Prop \"silly\" is required, but does not have example value"}] @@ -164,6 +204,24 @@ class JavaScriptComponentValidationTest extends BetterConfigEntityValidationTest $this->assertValidationErrors($expected_validation_errors); } + /** + * @testWith ["missing", "The JavaScript component with the machine name 'missing' does not exist."] + * ["", "The 'imported_js_components' contains an invalid component name."] + * ["🚀", "The 'imported_js_components' contains an invalid component name."] + * ["componente_extraño", "The 'imported_js_components' contains an invalid component name."] + * [";", "The 'imported_js_components' contains an invalid component name."] + */ + public function testNonExistingJsDependencies(string $component_id, string $expected_exception_message): void { + \assert($this->entity instanceof JavaScriptComponent); + $this->expectException(ConstraintViolationException::class); + $this->expectExceptionMessage($expected_exception_message); + + \assert($this->entity instanceof JavaScriptComponent); + $client_values = $this->entity->normalizeForClientSide()->values; + $client_values['imported_js_components'] = [$component_id]; + $this->entity->updateFromClientSide($client_values); + } + public static function providerInvalidEnumsAndExamples(): array { return [ 'Invalid string' => [ diff --git a/tests/src/Kernel/Config/JavascriptComponentTest.php b/tests/src/Kernel/Config/JavascriptComponentTest.php index b5628ec268707a270c3c43b4f2991579bc5dd309..9618b5af2376a98f452599abca38920bc2fbc85b 100644 --- a/tests/src/Kernel/Config/JavascriptComponentTest.php +++ b/tests/src/Kernel/Config/JavascriptComponentTest.php @@ -40,6 +40,9 @@ class JavascriptComponentTest extends KernelTestBase { $js_component = JavaScriptComponent::createFromClientSide($client_data); $this->assertSame(SAVED_NEW, $js_component->save()); $this->assertCount(0, $js_component->getDependencies()); + $this->assertSame([ + 'config:experience_builder.js_component.test', + ], $js_component->getCacheTags()); // Create another component that will be imported by the first one. $client_data_2 = $client_data; @@ -48,6 +51,9 @@ class JavascriptComponentTest extends KernelTestBase { $js_component2 = JavaScriptComponent::createFromClientSide($client_data_2); $this->assertSame(SAVED_NEW, $js_component2->save()); $this->assertCount(0, $js_component2->getDependencies()); + $this->assertSame([ + 'config:experience_builder.js_component.test2', + ], $js_component2->getCacheTags()); // Adding a component to `imported_js_components` should add this component // to the dependencies. @@ -60,6 +66,10 @@ class JavascriptComponentTest extends KernelTestBase { ], $js_component->getDependencies() ); + $this->assertSame([ + 'config:experience_builder.js_component.test', + 'config:experience_builder.js_component.test2', + ], $js_component->getCacheTags()); // Ensure missing components are will throw a validation error. $client_data['imported_js_components'] = [$js_component2->id(), 'missing']; @@ -74,7 +84,7 @@ class JavascriptComponentTest extends KernelTestBase { $this->assertCount(1, $violations); $violation = $violations->get(0); $this->assertSame('imported_js_components.1', $violation->getPropertyPath()); - $this->assertSame("The JavaScript component with machine name 'missing' does not exist.", $violation->getMessage()); + $this->assertSame("The JavaScript component with the machine name 'missing' does not exist.", $violation->getMessage()); } // Ensure not sending `imported_js_components` will throw an error. @@ -99,6 +109,9 @@ class JavascriptComponentTest extends KernelTestBase { $js_component->updateFromClientSide($client_data); $this->assertSame(SAVED_UPDATED, $js_component->save()); $this->assertSame([], $js_component->getDependencies()); + $this->assertSame([ + 'config:experience_builder.js_component.test', + ], $js_component->getCacheTags()); } } diff --git a/tests/src/Kernel/DataType/ComponentTreeHydratedTest.php b/tests/src/Kernel/DataType/ComponentTreeHydratedTest.php index 41ecfb797b1e4bde6ff005055b6df12b1c7dd4f3..390d0ff7f7a364558470f20efeefe31c65e83260 100644 --- a/tests/src/Kernel/DataType/ComponentTreeHydratedTest.php +++ b/tests/src/Kernel/DataType/ComponentTreeHydratedTest.php @@ -11,6 +11,7 @@ use Drupal\Core\Cache\CacheableMetadata; use Drupal\Core\Render\Markup; use Drupal\Core\Render\RendererInterface; use Drupal\Core\TypedData\TypedDataManagerInterface; +use Drupal\experience_builder\AutoSave\AutoSaveManager; use Drupal\experience_builder\Element\RenderSafeComponentContainer; use Drupal\experience_builder\Entity\Page; use Drupal\experience_builder\Plugin\DataType\ComponentTreeStructure; @@ -884,7 +885,10 @@ HTML, '#component' => [ '#type' => 'astro_island', '#cache' => [ - 'tags' => ['config:experience_builder.component.js.my-cta'], + 'tags' => [ + 'config:experience_builder.js_component.my-cta', + 'config:experience_builder.component.js.my-cta', + ], 'contexts' => [], 'max-age' => Cache::PERMANENT, ], @@ -941,7 +945,10 @@ HTML, '#component' => [ '#type' => 'astro_island', '#cache' => [ - 'tags' => ['config:experience_builder.component.js.my-cta-with-auto-save'], + 'tags' => [ + 'config:experience_builder.js_component.my-cta-with-auto-save', + 'config:experience_builder.component.js.my-cta-with-auto-save', + ], 'contexts' => [], 'max-age' => Cache::PERMANENT, ], @@ -1118,13 +1125,16 @@ HTML, 'expected_cache_tags' => [ 'config:experience_builder.component.sdc.xb_test_sdc.props-slots', 'config:experience_builder.component.sdc.xb_test_sdc.props-no-slots', + 'config:experience_builder.js_component.my-cta-with-auto-save', 'config:experience_builder.component.js.my-cta-with-auto-save', + 'config:experience_builder.js_component.my-cta', 'config:experience_builder.component.js.my-cta', 'config:system.site', 'config:experience_builder.component.block.system_branding_block', ], ]; yield 'component tree with complex nesting' => [...$component_tree_with_complex_nesting, 'isPreview' => FALSE]; + $path_to_auto_saved_js_component = [ ComponentTreeStructure::ROOT_UUID, 'uuid-in-root', @@ -1136,12 +1146,53 @@ HTML, 'uuid-js-component-auto-save', '#component', ]; + $path_to_js_component = [ + ComponentTreeStructure::ROOT_UUID, + 'uuid-in-root', + '#component', '#slots', 'the_body', + 'uuid-level-1', + '#component', '#slots', 'the_body', + 'uuid-level-2', + '#component', '#slots', 'the_body', + 'uuid-js-component', + '#component', + ]; yield 'component tree with complex nesting in preview' => [ ...self::overwriteRenderableExpectations( self::setIsPreviewPropertyRecursively($component_tree_with_complex_nesting, TRUE), [ - ['parents' => [...$path_to_auto_saved_js_component, '#name'], 'value' => 'My Code Component with Auto-Save - Draft'], - ['parents' => [...$path_to_auto_saved_js_component, '#component_url'], 'value' => '/xb/api/v0/auto-saves/js/js_component/my-cta-with-auto-save'], + [ + 'parents' => [...$path_to_auto_saved_js_component, '#name'], + 'value' => 'My Code Component with Auto-Save - Draft', + ], + [ + 'parents' => [...$path_to_auto_saved_js_component, '#component_url'], + 'value' => '/xb/api/v0/auto-saves/js/js_component/my-cta-with-auto-save', + ], + [ + 'parents' => [...$path_to_auto_saved_js_component, '#cache'], + 'value' => [ + 'tags' => [ + AutoSaveManager::CACHE_TAG, + 'config:experience_builder.js_component.my-cta-with-auto-save', + 'config:experience_builder.component.js.my-cta-with-auto-save', + ], + 'contexts' => [], + 'max-age' => Cache::PERMANENT, + ], + ], + [ + 'parents' => [...$path_to_js_component, '#cache'], + 'value' => [ + 'tags' => [ + AutoSaveManager::CACHE_TAG, + 'config:experience_builder.js_component.my-cta', + 'config:experience_builder.component.js.my-cta', + ], + 'contexts' => [], + 'max-age' => Cache::PERMANENT, + ], + ], ], ), 'expected_html' => <<<HTML @@ -1208,7 +1259,19 @@ HTML, <!-- xb-end-uuid-in-root --> HTML, 'isPreview' => TRUE, + 'expected_cache_tags' => [ + 'config:experience_builder.component.sdc.xb_test_sdc.props-slots', + 'config:experience_builder.component.sdc.xb_test_sdc.props-no-slots', + AutoSaveManager::CACHE_TAG, + 'config:experience_builder.js_component.my-cta-with-auto-save', + 'config:experience_builder.component.js.my-cta-with-auto-save', + 'config:experience_builder.js_component.my-cta', + 'config:experience_builder.component.js.my-cta', + 'config:system.site', + 'config:experience_builder.component.block.system_branding_block', + ], ]; + } /** diff --git a/tests/src/Kernel/DataType/ComponentTreeHydratedWithBlockOverrideTest.php b/tests/src/Kernel/DataType/ComponentTreeHydratedWithBlockOverrideTest.php index 860c373581798cd7ecbe4866e69a040ed8b63996..257934df6b878e96b36ac08c4b323bba4229a3fd 100644 --- a/tests/src/Kernel/DataType/ComponentTreeHydratedWithBlockOverrideTest.php +++ b/tests/src/Kernel/DataType/ComponentTreeHydratedWithBlockOverrideTest.php @@ -7,6 +7,7 @@ namespace Drupal\Tests\experience_builder\Kernel\DataType; // cspell:ignore ttxgk xpzur use Drupal\Core\Render\Element; +use Drupal\experience_builder\AutoSave\AutoSaveManager; use Drupal\experience_builder\Entity\JavaScriptComponent; use Drupal\experience_builder\Plugin\DataType\ComponentTreeStructure; use Drupal\Component\Utility\NestedArray; @@ -140,6 +141,7 @@ HTML, 'config:experience_builder.js_component.site_branding', 'config:system.site', 'config:experience_builder.component.block.system_branding_block', + AutoSaveManager::CACHE_TAG, ], ] + $original_test_case, @@ -241,7 +243,9 @@ HTML, 'expected_cache_tags' => [ 'config:experience_builder.component.sdc.xb_test_sdc.props-slots', 'config:experience_builder.component.sdc.xb_test_sdc.props-no-slots', + 'config:experience_builder.js_component.my-cta-with-auto-save', 'config:experience_builder.component.js.my-cta-with-auto-save', + 'config:experience_builder.js_component.my-cta', 'config:experience_builder.component.js.my-cta', 'config:experience_builder.js_component.site_branding', 'config:system.site', @@ -347,7 +351,10 @@ HTML, 'expected_cache_tags' => [ 'config:experience_builder.component.sdc.xb_test_sdc.props-slots', 'config:experience_builder.component.sdc.xb_test_sdc.props-no-slots', + AutoSaveManager::CACHE_TAG, + 'config:experience_builder.js_component.my-cta-with-auto-save', 'config:experience_builder.component.js.my-cta-with-auto-save', + 'config:experience_builder.js_component.my-cta', 'config:experience_builder.component.js.my-cta', 'config:experience_builder.js_component.site_branding', 'config:system.site', diff --git a/tests/src/Kernel/Plugin/ExperienceBuilder/ComponentSource/JsComponentTest.php b/tests/src/Kernel/Plugin/ExperienceBuilder/ComponentSource/JsComponentTest.php index 14c9316db90db6bd535a87a031720474b97e187d..f7746354b90ced59b28c0b0772ea6264b9ff58e5 100644 --- a/tests/src/Kernel/Plugin/ExperienceBuilder/ComponentSource/JsComponentTest.php +++ b/tests/src/Kernel/Plugin/ExperienceBuilder/ComponentSource/JsComponentTest.php @@ -11,6 +11,7 @@ use Drupal\Component\Utility\Crypt; use Drupal\Component\Utility\NestedArray; use Drupal\Core\Asset\AssetResolverInterface; use Drupal\Core\Asset\AttachedAssets; +use Drupal\Core\Cache\CacheableDependencyInterface; use Drupal\Core\Cache\CacheableMetadata; use Drupal\Core\File\FileUrlGeneratorInterface; use Drupal\Core\Site\Settings; @@ -123,6 +124,10 @@ final class JsComponentTest extends ComponentSourceTestBase { public static function getExpectedSettings(): array { return [ + 'js.xb_test_code_components_using_imports' => [ + 'local_source_id' => 'xb_test_code_components_using_imports', + 'prop_field_definitions' => [], + ], 'js.xb_test_code_components_vanilla_image' => [ 'local_source_id' => 'xb_test_code_components_vanilla_image', 'prop_field_definitions' => [ @@ -183,7 +188,7 @@ final class JsComponentTest extends ComponentSourceTestBase { // rendering. // @see ::testRenderComponent() $rendered_without_html = array_map( - fn ($expectations) => array_diff_key($expectations, ['html' => NULL]), + fn($expectations) => array_diff_key($expectations, ['html' => NULL]), $rendered, ); @@ -192,17 +197,30 @@ final class JsComponentTest extends ComponentSourceTestBase { 'theme', 'user.permissions', ]; + $default_cacheability = (new CacheableMetadata()) ->setCacheContexts($default_render_cache_contexts); + $this->assertEquals([ + 'js.xb_test_code_components_using_imports' => [ + 'cacheability' => (clone $default_cacheability) + ->setCacheTags([ + 'config:experience_builder.js_component.xb_test_code_components_using_imports', + 'config:experience_builder.js_component.xb_test_code_components_with_no_props', + 'config:experience_builder.js_component.xb_test_code_components_with_props', + ]), + ], 'js.xb_test_code_components_vanilla_image' => [ - 'cacheability' => $default_cacheability, + 'cacheability' => (clone $default_cacheability) + ->setCacheTags(['config:experience_builder.js_component.xb_test_code_components_vanilla_image']), ], 'js.xb_test_code_components_with_no_props' => [ - 'cacheability' => $default_cacheability, + 'cacheability' => (clone $default_cacheability) + ->setCacheTags(['config:experience_builder.js_component.xb_test_code_components_with_no_props']), ], 'js.xb_test_code_components_with_props' => [ - 'cacheability' => $default_cacheability, + 'cacheability' => (clone $default_cacheability) + ->setCacheTags(['config:experience_builder.js_component.xb_test_code_components_with_props']), ], ], $rendered_without_html); } @@ -211,18 +229,21 @@ final class JsComponentTest extends ComponentSourceTestBase { * For JavaScript components, auto-saves create an extra testing dimension! * * @depends testDiscovery - * @testWith [false, false, "live"] - * [false, true, "live"] - * [true, false, "live"] - * [true, true, "draft"] + * @testWith [false, false, "live", []] + * [false, true, "live", []] + * [true, false, "live", ["experience_builder__auto_save"]] + * [true, true, "draft", ["experience_builder__auto_save"]] */ - public function testRenderJsComponent(bool $preview_requested, bool $auto_save_exists, string $expected_result, array $component_ids): void { + public function testRenderJsComponent(bool $preview_requested, bool $auto_save_exists, string $expected_result, array $additional_expected_cache_tags, array $component_ids): void { $this->generateComponentConfig(); foreach ($this->componentStorage->loadMultiple($component_ids) as $component) { assert($component instanceof Component); $source = $component->getComponentSource(); \assert($source instanceof JsComponent); - $this->assertRenderedAstroIsland($component, $preview_requested, $auto_save_exists, $expected_result); + $expected_cacheability = (new CacheableMetadata()) + ->addCacheTags($additional_expected_cache_tags) + ->addCacheableDependency($source->getJavaScriptComponent()); + $this->assertRenderedAstroIsland($component, $preview_requested, $auto_save_exists, $expected_result, $expected_cacheability); } } @@ -241,6 +262,7 @@ final class JsComponentTest extends ComponentSourceTestBase { bool $preview_requested, bool $auto_save_exists, string $expected_result, + CacheableDependencyInterface $expected_cacheability, ): void { $source = $component->getComponentSource(); \assert($source instanceof JsComponent); @@ -254,18 +276,28 @@ final class JsComponentTest extends ComponentSourceTestBase { if ($auto_save_exists) { $this->container->get(AutoSaveManager::class) ->save( - $source->getJavaScriptComponent(), + $js_component, // 'imported_js_components' is a value sent by the client that is used to // determine Javascript Code component dependencies and is not saved // directly on the backend. + // Ensure that the current set of imported JS components continues to + // be respected. // @see \Drupal\experience_builder\Entity\JavaScriptComponent::addJavaScriptComponentsDependencies(). - $source->getJavaScriptComponent()->normalizeForClientSide()->values + ['imported_js_components' => []], + $js_component->normalizeForClientSide()->values + [ + 'imported_js_components' => array_map( + fn (string $config_name): string => str_replace('experience_builder.js_component.', '', $config_name), + $js_component->toArray()['dependencies']['enforced']['config'] ?? [] + ), + ], ); } $island = $source->renderComponent([ 'props' => $expected_component_props, ], 'some-uuid', $preview_requested); + + $this->assertEquals($expected_cacheability, CacheableMetadata::createFromRenderArray($island)); + $crawler = $this->crawlerForRenderArray($island); $element = $crawler->filter('astro-island'); @@ -332,6 +364,11 @@ final class JsComponentTest extends ComponentSourceTestBase { */ public function testCalculateDependencies(array $component_ids): void { self::assertSame([ + 'js.xb_test_code_components_using_imports' => [ + 'config' => [ + 'experience_builder.js_component.xb_test_code_components_using_imports', + ], + ], 'js.xb_test_code_components_vanilla_image' => [ 'module' => [ 'image', @@ -611,6 +648,17 @@ final class JsComponentTest extends ComponentSourceTestBase { */ public static function getExpectedClientSideInfo(): array { return [ + 'js.xb_test_code_components_using_imports' => [ + 'expected_output_selectors' => [ + 'astro-island[opts*="using imports"]', + 'script[blocking="render"][src*="/ui/lib/astro-hydration/dist/client.js"]', + ], + 'source' => 'Code component', + 'metadata' => ['slots' => []], + 'propSources' => [], + 'dynamic_prop_source_candidates' => [], + 'transforms' => [], + ], 'js.xb_test_code_components_vanilla_image' => [ 'expected_output_selectors' => [ 'astro-island[opts*="Vanilla Image"][props*="placehold.co"]',