og-8.x-1.x-dev/tests/src/Functional/OgAudienceHiddenGroupWidgetTest.php
tests/src/Functional/OgAudienceHiddenGroupWidgetTest.php
<?php
declare(strict_types=1);
namespace Drupal\Tests\og\Functional;
use Drupal\Component\Utility\UrlHelper;
use Drupal\node\Entity\Node;
use Drupal\node\NodeInterface;
use Drupal\og\Entity\OgRole;
use Drupal\og\Og;
use Drupal\og\OgGroupAudienceHelperInterface;
use Drupal\og\OgRoleInterface;
use Drupal\Tests\BrowserTestBase;
use Drupal\user\Entity\Role;
use Drupal\user\UserInterface;
/**
* Adds coverage to protect hidden audience references across widgets.
*
* @group og
*/
class OgAudienceHiddenGroupWidgetTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'node',
'og',
'og_ui',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Visible group one.
*/
protected NodeInterface $groupOne;
/**
* Visible group two.
*/
protected NodeInterface $groupTwo;
/**
* Hidden group (no membership for editor).
*/
protected NodeInterface $hiddenGroup;
/**
* Group content that references all three groups.
*/
protected NodeInterface $groupContent;
/**
* Additional audience field machine name.
*/
protected string $secondaryAudienceField = 'og_audience_secondary';
/**
* The two-group editor.
*/
protected UserInterface $editor;
/**
* Group owner.
*/
protected UserInterface $groupOwner;
/**
* OG role granting editor permissions within a group.
*/
protected OgRoleInterface $groupEditorRole;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->createContentType([
'name' => 'Group',
'type' => 'group',
]);
$this->createContentType([
'name' => 'Group content',
'type' => 'group_content',
]);
Og::addGroup('node', 'group');
$primary_settings = [
'form_display' => [
'type' => 'entity_reference_autocomplete',
],
];
Og::createField(OgGroupAudienceHelperInterface::DEFAULT_FIELD, 'node', 'group_content', $primary_settings);
$secondary_settings = [
'field_name' => $this->secondaryAudienceField,
'field_config' => [
'label' => 'Secondary groups audience',
],
'form_display' => [
'type' => 'options_select',
],
];
Og::createField(OgGroupAudienceHelperInterface::DEFAULT_FIELD, 'node', 'group_content', $secondary_settings);
$form_display = \Drupal::entityTypeManager()->getStorage('entity_form_display')->load('node.group_content.default');
$form_display->setComponent(OgGroupAudienceHelperInterface::DEFAULT_FIELD, [
'type' => 'entity_reference_autocomplete',
]);
$form_display->setComponent($this->secondaryAudienceField, [
'type' => 'options_select',
]);
$form_display->save();
$authenticated = Role::load('authenticated');
$authenticated
->grantPermission('create group_content content')
->grantPermission('edit any group_content content')
->save();
$this->groupOwner = $this->drupalCreateUser([], 'group owner');
$this->editor = $this->drupalCreateUser([], 'two group editor');
$this->groupOne = Node::create([
'type' => 'group',
'title' => 'Visible group one',
'uid' => $this->groupOwner->id(),
'status' => NodeInterface::PUBLISHED,
]);
$this->groupOne->save();
$this->groupTwo = Node::create([
'type' => 'group',
'title' => 'Visible group two',
'uid' => $this->groupOwner->id(),
'status' => NodeInterface::PUBLISHED,
]);
$this->groupTwo->save();
$this->hiddenGroup = Node::create([
'type' => 'group',
'title' => 'Hidden group three',
'uid' => $this->groupOwner->id(),
'status' => NodeInterface::PUBLISHED,
]);
$this->hiddenGroup->save();
$this->groupEditorRole = OgRole::create();
$this->groupEditorRole
->setName('group_content_editor')
->setLabel('Group content editor')
->setGroupType('node')
->setGroupBundle('group')
->grantPermission('create group_content content')
->grantPermission('edit any group_content content')
->save();
Og::createMembership($this->groupOne, $this->editor)
->addRole($this->groupEditorRole)
->save();
Og::createMembership($this->groupTwo, $this->editor)
->addRole($this->groupEditorRole)
->save();
$this->groupContent = Node::create([
'type' => 'group_content',
'title' => 'Content referencing hidden group',
'uid' => $this->groupOwner->id(),
'status' => NodeInterface::PUBLISHED,
OgGroupAudienceHelperInterface::DEFAULT_FIELD => [
['target_id' => $this->groupOne->id()],
['target_id' => $this->groupTwo->id()],
['target_id' => $this->hiddenGroup->id()],
],
$this->secondaryAudienceField => [
['target_id' => $this->groupOne->id()],
['target_id' => $this->groupTwo->id()],
['target_id' => $this->hiddenGroup->id()],
],
]);
$this->groupContent->save();
}
/**
* Select widget should not drop hidden groups on submit.
*/
public function testOptionsSelectPreservesHiddenGroup(): void {
$this->configureAudienceWidget('options_select');
$this->drupalLogin($this->editor);
$this->drupalGet($this->getEditPath());
$this->assertSession()->optionExists('Groups audience', $this->groupOne->label());
$this->assertSession()->optionExists('Groups audience', $this->groupTwo->label());
$this->assertSession()->optionNotExists('Groups audience', $this->hiddenGroup->label());
$this->submitForm([], 'Save');
$this->assertSession()->pageTextContains('has been updated.');
$this->assertHiddenGroupReferencePreserved();
}
/**
* Checkbox/radio widget should not drop hidden groups on submit.
*/
public function testOptionsButtonsPreservesHiddenGroup(): void {
$this->configureAudienceWidget('options_buttons');
$this->drupalLogin($this->editor);
$this->drupalGet($this->getEditPath());
$this->assertSession()->responseContains($this->groupOne->label());
$this->assertSession()->responseContains($this->groupTwo->label());
$this->assertSession()->elementNotExists('css', '#edit-og-audience input[value="' . $this->hiddenGroup->id() . '"]');
$this->submitForm([], 'Save');
$this->assertSession()->pageTextContains('has been updated.');
$this->assertHiddenGroupReferencePreserved();
}
/**
* Autocomplete widget should hide labels and preserve hidden groups.
*/
public function testAutocompleteWidgetPreservesHiddenGroup(): void {
$this->configureAudienceWidget('entity_reference_autocomplete');
$this->drupalLogin($this->editor);
$edit_path = $this->getEditPath();
$this->drupalGet($edit_path);
$this->assertSession()->responseContains($this->groupOne->label());
$this->assertSession()->responseNotContains($this->hiddenGroup->label());
$input = $this->assertSession()->elementExists('css', '[data-drupal-selector^="edit-og-audience"] input[data-autocomplete-path]');
$autocomplete_path = $input->getAttribute('data-autocomplete-path');
$this->assertNotEmpty($autocomplete_path, 'Autocomplete path is present for og_audience.');
$this->assertHiddenGroupAbsentFromAutocomplete($autocomplete_path);
$this->drupalGet($edit_path);
$this->submitForm([], 'Save');
$this->assertSession()->pageTextContains('has been updated.');
$this->assertHiddenGroupReferencePreserved();
}
/**
* Tags autocomplete widget should hide labels and preserve hidden groups.
*/
public function testAutocompleteTagsWidgetPreservesHiddenGroup(): void {
$this->configureAudienceWidget('entity_reference_autocomplete_tags');
$this->drupalLogin($this->editor);
$edit_path = $this->getEditPath();
$this->drupalGet($edit_path);
$this->assertSession()->responseContains($this->groupOne->label());
$this->assertSession()->responseNotContains($this->hiddenGroup->label());
$input = $this->assertSession()->elementExists('css', '[data-drupal-selector^="edit-og-audience"] input[data-autocomplete-path]');
$autocomplete_path = $input->getAttribute('data-autocomplete-path');
$this->assertNotEmpty($autocomplete_path, 'Autocomplete path is present for og_audience tags widget.');
$this->assertHiddenGroupAbsentFromAutocomplete($autocomplete_path);
$this->drupalGet($edit_path);
$this->submitForm([], 'Save');
$this->assertSession()->pageTextContains('has been updated.');
$this->assertHiddenGroupReferencePreserved();
}
/**
* Secondary audience field should hide and preserve hidden groups.
*/
public function testSecondaryOptionsFieldPreservesHiddenGroup(): void {
$this->configureAudienceWidget('options_select');
$this->drupalLogin($this->editor);
$edit_path = $this->getEditPath();
$this->drupalGet($edit_path);
$this->assertSession()->elementExists('css', $this->getSecondarySelectSelector());
$this->assertSecondaryOption((int) $this->groupOne->id(), TRUE);
$this->assertSecondaryOption((int) $this->groupTwo->id(), TRUE);
$this->assertSecondaryOption((int) $this->hiddenGroup->id(), FALSE);
$this->submitForm([], 'Save');
$this->assertSession()->pageTextContains('has been updated.');
$this->assertHiddenGroupReferencePreserved();
}
/**
* Adjust the widget type for the audience field.
*/
private function configureAudienceWidget(string $widget_type): void {
$storage = \Drupal::entityTypeManager()->getStorage('entity_form_display');
$form_display = $storage->load('node.group_content.default');
$component = $form_display->getComponent(OgGroupAudienceHelperInterface::DEFAULT_FIELD);
$component['type'] = $widget_type;
$component['settings'] = $component['settings'] ?? [];
$form_display->setComponent(OgGroupAudienceHelperInterface::DEFAULT_FIELD, $component);
$form_display->save();
}
/**
* Path to the edit form of the prepared group content.
*/
private function getEditPath(): string {
return $this->groupContent->toUrl('edit-form')->toString();
}
/**
* Hidden group should remain referenced after a form submission.
*/
private function assertHiddenGroupReferencePreserved(): void {
$expected = array_map('intval', [
$this->groupOne->id(),
$this->groupTwo->id(),
$this->hiddenGroup->id(),
]);
sort($expected);
$storage = \Drupal::entityTypeManager()->getStorage('node');
$storage->resetCache([$this->groupContent->id()]);
$reloaded = $storage->load($this->groupContent->id());
self::assertInstanceOf(NodeInterface::class, $reloaded);
$actual = array_map(static fn(array $item): int => (int) $item['target_id'], $reloaded->get(OgGroupAudienceHelperInterface::DEFAULT_FIELD)->getValue());
sort($actual);
self::assertSame($expected, $actual, 'Hidden group reference persists after save in primary field.');
$secondary = array_map(static fn(array $item): int => (int) $item['target_id'], $reloaded->get($this->secondaryAudienceField)->getValue());
sort($secondary);
self::assertSame($expected, $secondary, 'Hidden group reference persists after save in secondary field.');
}
/**
* Assert the autocomplete callback does not expose the hidden group.
*/
private function assertHiddenGroupAbsentFromAutocomplete(string $autocomplete_path): void {
$parsed = UrlHelper::parse($autocomplete_path);
$query = $parsed['query'] ?? [];
$query['q'] = $this->hiddenGroup->label();
$this->drupalGet($parsed['path'], ['query' => $query]);
$this->assertSession()->statusCodeEquals(200);
$response = json_decode($this->getSession()->getPage()->getContent(), TRUE);
self::assertIsArray($response);
foreach ($response as $suggestion) {
$label = (string) ($suggestion['label'] ?? '');
self::assertStringNotContainsString($this->hiddenGroup->label(), $label);
}
}
/**
* Returns the CSS selector for the secondary audience select element.
*/
private function getSecondarySelectSelector(): string {
return sprintf('[data-drupal-selector^="edit-%s"]', str_replace('_', '-', $this->secondaryAudienceField));
}
/**
* Assert that the secondary audience select contains or omits a group.
*/
private function assertSecondaryOption(int $group_id, bool $should_be_present): void {
$selector = $this->getSecondarySelectSelector() . sprintf(' option[value="%d"]', $group_id);
if ($should_be_present) {
$this->assertSession()->elementExists('css', $selector);
}
else {
$this->assertSession()->elementNotExists('css', $selector);
}
}
}
