diff --git a/src/Plugin/ExperienceBuilder/ComponentSource/JsComponent.php b/src/Plugin/ExperienceBuilder/ComponentSource/JsComponent.php index 0f4c596109a688a56774e7cb40d9ff95c5eeffdd..281265d44e0d11879802ca26344ad539bb1fa389 100644 --- a/src/Plugin/ExperienceBuilder/ComponentSource/JsComponent.php +++ b/src/Plugin/ExperienceBuilder/ComponentSource/JsComponent.php @@ -254,6 +254,7 @@ final class JsComponent extends GeneratedFieldExplicitInputUxComponentSourceBase $label_key = $component->getEntityType()->getKey('label'); assert(is_string($label_key)); $component->set($label_key, $js_component->label()); + $component->setStatus($js_component->status()); try { $ephemeral_sdc_component = self::buildEphemeralSdcPluginInstance($js_component); } diff --git a/tests/src/Functional/XbConfigEntityHttpApiTest.php b/tests/src/Functional/XbConfigEntityHttpApiTest.php index 003613ecb3ecfea190de7158207d94c84bb8ca9c..ec725f017b410c1c4751b46b13355e9b97d770f8 100644 --- a/tests/src/Functional/XbConfigEntityHttpApiTest.php +++ b/tests/src/Functional/XbConfigEntityHttpApiTest.php @@ -6,6 +6,7 @@ namespace Drupal\Tests\experience_builder\Functional; use Drupal\Core\Url; use Drupal\experience_builder\Entity\AssetLibrary; +use Drupal\experience_builder\Entity\Component; use Drupal\experience_builder\Entity\Pattern; use Drupal\system\Entity\Menu; use Drupal\Tests\experience_builder\Traits\ContribStrictConfigSchemaTestTrait; @@ -31,6 +32,7 @@ class XbConfigEntityHttpApiTest extends HttpApiTestBase { 'block', 'experience_builder', 'xb_test_sdc', + 'node', ]; /** @@ -602,6 +604,10 @@ class XbConfigEntityHttpApiTest extends HttpApiTestBase { 'js_footer' => '', ]; $this->assertSame($expected_component, $body); + // Confirm that the code component IS NOT exposed. + // @see docs/config-management.md#3.2.1 + $this->assertExposedCodeComponents([], 'MISS', $request_options); + $this->assertExposedCodeComponents([], 'HIT', $request_options); // Confirm no auto-save entity has been created. $this->assertExpectedResponse('GET', $auto_save_url, $request_options, 204, ['user.permissions'], ['experience_builder__autosave', 'http_response'], 'UNCACHEABLE (request policy)', 'MISS'); $this->assertExpectedResponse('GET', $auto_save_url, $request_options, 204, ['user.permissions'], ['experience_builder__autosave', 'http_response'], 'UNCACHEABLE (request policy)', 'HIT'); @@ -678,14 +684,46 @@ class XbConfigEntityHttpApiTest extends HttpApiTestBase { 'http_response', ], 'UNCACHEABLE (request policy)', 'MISS'); $this->assertSame(['test' => $expected_component], $body); + // Confirm that the code component IS STILL NOT exposed, because `status` is + // still `FALSE`. + // @see docs/config-management.md#3.2.1 + $this->assertExposedCodeComponents([], 'HIT', $request_options); - // Modify a Code Component incorrectly (consistency-wise): 422. - // @todo This currently returns a 200 response! https://www.drupal.org/i/3500043 will disallow PATCHing this if > 0 uses of this component exist. + // Modify a Code Component correctly: 200. + // ⚠️This is changing it from `internal` → `exposed`, for the first time, + // this must trigger the creation a corresponding `Component` config entity. + $this->assertNull(Component::load('js.test')); + // @todo https://www.drupal.org/i/3500043 will disallow PATCHing this if > 0 uses of this component exist. $code_component_to_send['status'] = TRUE; $expected_component['status'] = TRUE; $request_options[RequestOptions::BODY] = self::encodeXBData($code_component_to_send); $body = $this->assertExpectedResponse('PATCH', Url::fromUri('base:/xb/api/config/js_component/test'), $request_options, 200, NULL, NULL, NULL, NULL); $this->assertSame($expected_component, $body); + // Confirm that the code component IS exposed, because `status` was just + // changed to `TRUE`. + // @see docs/config-management.md#3.2.1 + $this->assertNotNull(Component::load('js.test')); + $this->assertExposedCodeComponents(['js.test'], 'MISS', $request_options); + $this->assertExposedCodeComponents(['js.test'], 'HIT', $request_options); + + // Modify a Code Component correctly: 200. + // ⚠️This is changing it from `exposed` → `internal`. This must cause the + // `Component` config entity to continue to exist, but get its `status` to + // change to `FALSE`, and cause it to be omitted from the list of available + // components for the Content Creator. + // @todo https://www.drupal.org/i/3500043 will disallow PATCHing this if > 0 uses of this component exist. + $code_component_to_send['status'] = FALSE; + $expected_component['status'] = FALSE; + $request_options[RequestOptions::BODY] = self::encodeXBData($code_component_to_send); + $body = $this->assertExpectedResponse('PATCH', Url::fromUri('base:/xb/api/config/js_component/test'), $request_options, 200, NULL, NULL, NULL, NULL); + $this->assertSame($expected_component, $body); + // Confirm that the code component IS exposed, because `status` was just + // changed to `TRUE`. + // @see docs/config-management.md#3.2.1 + $this->assertNotNull(Component::load('js.test')); + $this->assertFalse(Component::load('js.test')->status()); + $this->assertExposedCodeComponents([], 'MISS', $request_options); + $this->assertExposedCodeComponents([], 'HIT', $request_options); // Create an auto-save entry for this config entity, to verify that neither // the "list" nor the "individual" API responses tested here are affected by @@ -709,6 +747,11 @@ class XbConfigEntityHttpApiTest extends HttpApiTestBase { // Delete the 'test' Code Component via the XB HTTP API: 204. $body = $this->assertExpectedResponse('DELETE', Url::fromUri('base:/xb/api/config/js_component/test'), [], 204, NULL, NULL, NULL, NULL); $this->assertNull($body); + // Confirm that the code component IS NOT exposed, because it no longer + // exists. + // @see docs/config-management.md#3.2.1 + $this->assertExposedCodeComponents([], 'MISS', $request_options); + $this->assertExposedCodeComponents([], 'HIT', $request_options); // Re-retrieve list: 200, empty list. Dynamic Page Cache miss. $body = $this->assertExpectedResponse('GET', $list_url, [], 200, ['user.permissions'], ['config:js_component_list', 'http_response'], 'UNCACHEABLE (request policy)', 'MISS'); @@ -839,4 +882,46 @@ class XbConfigEntityHttpApiTest extends HttpApiTestBase { ], json_decode((string) $response->getBody(), TRUE)); } + private function assertExposedCodeComponents(array $expected, string $expected_dynamic_page_cache, array $request_options): void { + assert(in_array($expected_dynamic_page_cache, ['HIT', 'MISS'], TRUE)); + $expected_contexts = [ + 'languages:language_content', + 'languages:language_interface', + 'route', + 'theme', + 'url.path', + 'url.query_args', + 'user.node_grants:view', + 'user.permissions', + 'user.roles:authenticated', + // The user_login_block is rendered as the anonymous user because for the + // authenticated user it is empty. + // @see \Drupal\experience_builder\Controller\ApiComponentsController::getCacheableClientSideInfo() + 'user.roles:anonymous', + ]; + $body = $this->assertExpectedResponse('GET', Url::fromUri('base:/xb/api/config/component'), $request_options, 200, $expected_contexts, [ + 'CACHE_MISS_IF_UNCACHEABLE_HTTP_METHOD:form', + 'config:component_list', + 'config:core.extension', + 'config:node_type_list', + 'config:system.menu.account', + 'config:system.site', + 'config:system.theme', + 'config:views.view.content_recent', + 'config:views.view.who_s_new', + 'http_response', + 'local_task', + 'node_list', + 'user:1', + 'user:2', + 'user_list', + ], 'UNCACHEABLE (request policy)', $expected_dynamic_page_cache); + self:self::assertNotNull($body); + $component_config_entity_ids = array_keys($body); + self::assertSame( + $expected, + array_values(array_filter($component_config_entity_ids, fn (string $id) => str_starts_with($id, 'js.'))), + ); + } + } diff --git a/tests/src/Kernel/Config/JavascriptComponentStorageTest.php b/tests/src/Kernel/Config/JavascriptComponentStorageTest.php index f6e57148609c05926653c8f8d5ea805d8b842680..da04409b6fe669552250e92cc652fead5520145e 100644 --- a/tests/src/Kernel/Config/JavascriptComponentStorageTest.php +++ b/tests/src/Kernel/Config/JavascriptComponentStorageTest.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Drupal\Tests\experience_builder\Kernel\Config; +use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\experience_builder\ComponentIncompatibilityReasonRepository; use Drupal\experience_builder\Entity\Component; use Drupal\experience_builder\Entity\ComponentInterface; @@ -16,7 +17,8 @@ use Drupal\Tests\user\Traits\UserCreationTrait; * Tests JavascriptComponentStorage. * * @covers \Drupal\experience_builder\EntityHandlers\JavascriptComponentStorage - * @covers \Drupal\experience_builder\Plugin\ExperienceBuilder\ComponentSource\JsComponent + * @covers \Drupal\experience_builder\Plugin\ExperienceBuilder\ComponentSource\JsComponent::createConfigEntity + * @covers \Drupal\experience_builder\Plugin\ExperienceBuilder\ComponentSource\JsComponent::updateConfigEntity * @group JavaScriptComponents * @group experience_builder */ @@ -74,9 +76,9 @@ final class JavascriptComponentStorageTest extends AssetLibraryStorageTest { } /** - * Covers component creation. + * @covers \Drupal\experience_builder\Plugin\ExperienceBuilder\ComponentSource\JsComponent::createConfigEntity() */ - public function testComponentEntityCreation(): void { + public function testComponentEntityCreation(): array { $js_component_id = $this->randomMachineName(); $component_id = JsComponent::componentIdFromJavascriptComponentId($js_component_id); $reason_repository = $this->container->get(ComponentIncompatibilityReasonRepository::class); @@ -174,10 +176,44 @@ final class JavascriptComponentStorageTest extends AssetLibraryStorageTest { $js_component->set('name', $new_name); $js_component->setProps($props)->save(); - $component = \Drupal::entityTypeManager()->getStorage(Component::ENTITY_TYPE_ID)->loadUnchanged($component_id); - \assert($component instanceof ComponentInterface); + $component = $this->loadComponent($component_id); self::assertEquals($new_name, $component->label()); self::assertEquals(['noodles', 'title'], \array_keys($component->getSettings()['prop_field_definitions'])); + + return $js_component->toArray(); + } + + /** + * @covers \Drupal\experience_builder\Plugin\ExperienceBuilder\ComponentSource\JsComponent::updateConfigEntity() + * @depends testComponentEntityCreation + */ + public function testComponentEntityUpdate(array $js_component_values): void { + $js_component = JavaScriptComponent::create($js_component_values); + $js_component->save(); + assert(is_string($js_component->id())); + $component_id = JsComponent::componentIdFromJavascriptComponentId($js_component->id()); + + // Name should carry over. + $new_name = $js_component->label() . ' — updated'; + $js_component->set('name', $new_name)->save(); + $this->assertSame($new_name, $this->loadComponent($component_id)->label()); + + // Status should carry over. + $this->assertTrue($js_component->status()); + $this->assertTrue($this->loadComponent($component_id)->status()); + $js_component->disable()->save(); + $this->assertFalse($js_component->status()); + $this->assertFalse($this->loadComponent($component_id)->status()); + $js_component->enable()->save(); + $this->assertTrue($js_component->status()); + $this->assertTrue($this->loadComponent($component_id)->status()); + } + + private function loadComponent(string $id): Component { + // @phpstan-ignore-next-line + return $this->container->get(EntityTypeManagerInterface::class) + ->getStorage(Component::ENTITY_TYPE_ID) + ->loadUnchanged($id); } }