bulk_edit_terms-8.x-1.1/tests/src/Functional/BulkEditTermsTest.php
tests/src/Functional/BulkEditTermsTest.php
<?php
declare(strict_types=1);
namespace Drupal\Tests\bulk_edit_terms\Functional;
use Drupal\Core\Field\EntityReferenceFieldItemListInterface;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\node\Entity\Node;
use Drupal\node\NodeInterface;
use Drupal\taxonomy\Entity\Term;
use Drupal\taxonomy\Entity\Vocabulary;
use Drupal\taxonomy\TermInterface;
use Drupal\Tests\BrowserTestBase;
/**
* Tests the edit term assignment action plugin via views bulk operations.
*/
class BulkEditTermsTest extends BrowserTestBase {
/**
* {@inheritDoc}
*/
protected static $modules = ['node', 'taxonomy', 'views', 'bulk_edit_terms'];
/**
* {@inheritDoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritDoc}
*/
protected function setUp(): void {
parent::setUp();
$vocab = Vocabulary::create([
'vid' => 'tag',
'name' => 'Tag',
]);
$vocab->save();
$vocab = Vocabulary::create([
'vid' => 'category',
'name' => 'Category',
]);
$vocab->save();
// Create an Article content type that has both a
// multi-value tag term ref field to Tag and a single-value
// term ref field to Category.
$this->drupalCreateContentType([
'type' => 'article',
'name' => 'Article',
]);
$field_storage = FieldStorageConfig::create([
'field_name' => 'field_tags',
'entity_type' => 'node',
'translatable' => FALSE,
'entity_types' => [],
'settings' => [
'target_type' => 'taxonomy_term',
],
'type' => 'entity_reference',
'cardinality' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED,
]);
$field_storage->save();
$field = FieldConfig::create([
'field_name' => 'field_tags',
'entity_type' => 'node',
'bundle' => 'article',
'label' => 'Tags',
'settings' => [
'handler' => 'default',
'handler_settings' => [
'target_bundles' => [
'tag',
],
],
],
]);
$field->save();
$field_storage = FieldStorageConfig::create([
'field_name' => 'field_category',
'entity_type' => 'node',
'translatable' => FALSE,
'entity_types' => [],
'settings' => [
'target_type' => 'taxonomy_term',
],
'type' => 'entity_reference',
'cardinality' => 1,
]);
$field_storage->save();
$field = FieldConfig::create([
'field_name' => 'field_category',
'entity_type' => 'node',
'bundle' => 'article',
'label' => 'Category',
'settings' => [
'handler' => 'default',
'handler_settings' => [
'target_bundles' => [
'category',
],
],
],
]);
$field->save();
$entity_display = \Drupal::service('entity_display.repository');
$entity_display->getViewDisplay('node', 'article', 'default')
->setComponent('field_tags')
->save();
$entity_display->getViewDisplay('node', 'article', 'default')
->setComponent('field_category')
->save();
// Create a Page content type that has a multi-value tags field only.
$this->drupalCreateContentType([
'type' => 'page',
'name' => 'Page',
]);
$field = FieldConfig::create([
'field_name' => 'field_tags',
'entity_type' => 'node',
'bundle' => 'page',
'label' => 'Tags',
'settings' => [
'handler' => 'default',
'handler_settings' => [
'target_bundles' => [
'tag',
],
],
],
'cardinality' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED,
]);
$field->save();
$entity_display->getViewDisplay('node', 'page', 'default')
->setComponent('field_tags')
->save();
// Create some terms in each vocabulary.
$tag1 = Term::create([
'name' => 'tag1',
'vid' => 'tag',
]);
$tag1->save();
$tag2 = Term::create([
'name' => 'tag2',
'vid' => 'tag',
]);
$tag2->save();
$tag3 = Term::create([
'name' => 'tag3',
'vid' => 'tag',
]);
$tag3->save();
$cat1 = Term::create([
'name' => 'cat1',
'vid' => 'category',
]);
$cat1->save();
$cat2 = Term::create([
'name' => 'cat2',
'vid' => 'category',
]);
$cat2->save();
// Create some nodes. Set specific created & changed timestamps to ensure
// the order they display in the content view. This allows us to easily
// know which nodes we're selecting with the bulk checkboxes on the form.
// First, an article with no existing term references.
$article1 = Node::create([
'type' => 'article',
'title' => 'Test Article 1',
'created' => \Drupal::time()->getRequestTime() - 30,
'changed' => \Drupal::time()->getRequestTime() - 30,
]);
$article1->save();
// An article with existing references.
$article2 = Node::create([
'type' => 'article',
'title' => 'Test Article 2',
'field_tags' => [$tag1, $tag3],
'field_category' => $cat1,
'created' => \Drupal::time()->getRequestTime() - 20,
'changed' => \Drupal::time()->getRequestTime() - 20,
]);
$article2->save();
// A page with no existing references.
$page1 = Node::create([
'type' => 'page',
'title' => 'Test Page 1',
'created' => \Drupal::time()->getRequestTime() - 10,
'changed' => \Drupal::time()->getRequestTime() - 10,
]);
$page1->save();
// Baseline assertions to ensure our term references were set.
$this->assertFalse($this->hasTaxonomyTermInReferenceField($article1, 'field_tags', (int) $tag1->id()));
$this->assertFalse($this->hasTaxonomyTermInReferenceField($article1, 'field_tags', (int) $tag2->id()));
$this->assertFalse($this->hasTaxonomyTermInReferenceField($article1, 'field_category', (int) $cat1->id()));
$this->assertFalse($this->hasTaxonomyTermInReferenceField($article1, 'field_category', (int) $cat2->id()));
$this->assertTrue($this->hasTaxonomyTermInReferenceField($article2, 'field_tags', (int) $tag1->id()));
$this->assertFalse($this->hasTaxonomyTermInReferenceField($article2, 'field_tags', (int) $tag2->id()));
$this->assertTrue($this->hasTaxonomyTermInReferenceField($article2, 'field_category', (int) $cat1->id()));
$this->assertFalse($this->hasTaxonomyTermInReferenceField($article2, 'field_category', (int) $cat2->id()));
$this->assertFalse($this->hasTaxonomyTermInReferenceField($page1, 'field_tags', (int) $tag1->id()));
$this->assertFalse($this->hasTaxonomyTermInReferenceField($page1, 'field_tags', (int) $tag2->id()));
$admin_user = $this->drupalCreateUser([
'access content overview',
'administer bulk edit terms',
'administer content types',
'administer nodes',
'bypass node access',
]);
$this->drupalLogin($admin_user);
}
/**
* Tests the Clear action.
*/
public function testClearAction(): void {
// Login as administrator and go to admin/content.
$this->drupalGet('admin/content');
$this->assertSession()->pageTextContains('Test Article 1');
$this->assertSession()->pageTextContains('Test Article 2');
$this->assertSession()->pageTextContains('Test Page 1');
// Let's edit all three of them.
$this->submitForm([
'node_bulk_form[0]' => TRUE,
'node_bulk_form[1]' => TRUE,
'node_bulk_form[2]' => TRUE,
'action' => 'node_edit_terms_action',
], 'Apply to selected items');
$this->submitForm([
// This is an autocomplete tags field, so we need to write out the name
// and TID as it would be filled out via autocomplete.
'field_tags_action' => 'clear',
'field_category_action' => 'clear',
], 'Update terms');
// Only the one node that had any term assignments should have been
// updated.
$this->assertSession()->pageTextContains('1 node was updated.');
// Confirm the tag updates stuck. Reload entities from storage so they
// get the updates.
$article1 = $this->getNodeByTitle('Test Article 1', TRUE);
$article2 = $this->getNodeByTitle('Test Article 2', TRUE);
$page1 = $this->getNodeByTitle('Test Page 1', TRUE);
// The reference fields should be empty now on all nodes.
$this->assertTrue($article1->get('field_tags')->isEmpty());
$this->assertTrue($article2->get('field_tags')->isEmpty());
$this->assertTrue($page1->get('field_tags')->isEmpty());
$this->assertTrue($article1->get('field_category')->isEmpty());
$this->assertTrue($article2->get('field_category')->isEmpty());
}
/**
* Tests the Replace action.
*/
public function testReplaceAction(): void {
$tag1 = $this->getTermByName('tag1');
$tag2 = $this->getTermByName('tag2');
$tag3 = $this->getTermByName('tag3');
$cat1 = $this->getTermByName('cat1');
$cat2 = $this->getTermByName('cat2');
// Login as administrator and go to admin/content.
$this->drupalGet('admin/content');
$this->assertSession()->pageTextContains('Test Article 1');
$this->assertSession()->pageTextContains('Test Article 2');
$this->assertSession()->pageTextContains('Test Page 1');
// Let's edit all three of them.
$this->submitForm([
'node_bulk_form[0]' => TRUE,
'node_bulk_form[1]' => TRUE,
'node_bulk_form[2]' => TRUE,
'action' => 'node_edit_terms_action',
], 'Apply to selected items');
$this->assertSession()->fieldExists('field_tags');
$this->assertSession()->fieldExists('field_category');
$this->submitForm([
// This is an autocomplete tags field, so we need to write out the name
// and TID as it would be filled out via autocomplete.
'field_tags' => "{$tag2->label()} ({$tag2->id()})",
'field_tags_action' => 'replace',
'field_category' => $cat2->id(),
'field_category_action' => 'replace',
], 'Update terms');
$this->assertSession()->pageTextContains('3 nodes were updated.');
// Confirm the tag updates stuck. Reload entities from storage so they
// get the updates.
$article1 = $this->getNodeByTitle('Test Article 1', TRUE);
$article2 = $this->getNodeByTitle('Test Article 2', TRUE);
$page1 = $this->getNodeByTitle('Test Page 1', TRUE);
$this->assertFalse($this->hasTaxonomyTermInReferenceField($article1, 'field_tags', (int) $tag1->id()));
$this->assertTrue($this->hasTaxonomyTermInReferenceField($article1, 'field_tags', (int) $tag2->id()));
$this->assertFalse($this->hasTaxonomyTermInReferenceField($article1, 'field_tags', (int) $tag3->id()));
$this->assertFalse($this->hasTaxonomyTermInReferenceField($article1, 'field_category', (int) $cat1->id()));
$this->assertTrue($this->hasTaxonomyTermInReferenceField($article1, 'field_category', (int) $cat2->id()));
$this->assertFalse($this->hasTaxonomyTermInReferenceField($article2, 'field_tags', (int) $tag1->id()));
$this->assertTrue($this->hasTaxonomyTermInReferenceField($article2, 'field_tags', (int) $tag2->id()));
$this->assertFalse($this->hasTaxonomyTermInReferenceField($article2, 'field_tags', (int) $tag3->id()));
$this->assertFalse($this->hasTaxonomyTermInReferenceField($article2, 'field_category', (int) $cat1->id()));
$this->assertTrue($this->hasTaxonomyTermInReferenceField($article2, 'field_category', (int) $cat2->id()));
$this->assertFalse($this->hasTaxonomyTermInReferenceField($page1, 'field_tags', (int) $tag1->id()));
$this->assertTrue($this->hasTaxonomyTermInReferenceField($page1, 'field_tags', (int) $tag2->id()));
$this->assertFalse($this->hasTaxonomyTermInReferenceField($page1, 'field_tags', (int) $tag3->id()));
}
/**
* Tests add action.
*/
public function testAddAction(): void {
$tag1 = $this->getTermByName('tag1');
$tag2 = $this->getTermByName('tag2');
$tag3 = $this->getTermByName('tag3');
// Login as administrator and go to admin/content.
$this->drupalGet('admin/content');
$this->assertSession()->pageTextContains('Test Article 1');
$this->assertSession()->pageTextContains('Test Article 2');
$this->assertSession()->pageTextContains('Test Page 1');
// Let's edit all three of them.
$this->submitForm([
'node_bulk_form[0]' => TRUE,
'node_bulk_form[1]' => TRUE,
'node_bulk_form[2]' => TRUE,
'action' => 'node_edit_terms_action',
], 'Apply to selected items');
$this->assertSession()->fieldExists('field_tags');
$this->assertSession()->fieldExists('field_category');
// Don't submit anything for the single-value category field, but confirm
// that "add" is not an option for it.
$this->submitForm([
'field_tags' => "{$tag2->label()} ({$tag2->id()})",
'field_tags_action' => 'append',
'field_category_action' => 'none',
], 'Update terms');
$this->assertSession()->pageTextContains('3 nodes were updated.');
// Confirm the tag updates stuck. Reload entities from storage so they
// get the updates.
$article1 = $this->getNodeByTitle('Test Article 1', TRUE);
$article2 = $this->getNodeByTitle('Test Article 2', TRUE);
$page1 = $this->getNodeByTitle('Test Page 1', TRUE);
$this->assertFalse($this->hasTaxonomyTermInReferenceField($article1, 'field_tags', (int) $tag1->id()));
$this->assertTrue($this->hasTaxonomyTermInReferenceField($article1, 'field_tags', (int) $tag2->id()));
$this->assertFalse($this->hasTaxonomyTermInReferenceField($article1, 'field_tags', (int) $tag3->id()));
$this->assertTrue($this->hasTaxonomyTermInReferenceField($article2, 'field_tags', (int) $tag1->id()));
$this->assertTrue($this->hasTaxonomyTermInReferenceField($article2, 'field_tags', (int) $tag2->id()));
$this->assertTrue($this->hasTaxonomyTermInReferenceField($article2, 'field_tags', (int) $tag3->id()));
$this->assertFalse($this->hasTaxonomyTermInReferenceField($page1, 'field_tags', (int) $tag1->id()));
$this->assertTrue($this->hasTaxonomyTermInReferenceField($page1, 'field_tags', (int) $tag2->id()));
$this->assertFalse($this->hasTaxonomyTermInReferenceField($page1, 'field_tags', (int) $tag3->id()));
}
/**
* Tests remove action.
*/
public function testRemoveAction(): void {
$tag1 = $this->getTermByName('tag1');
$tag2 = $this->getTermByName('tag2');
$tag3 = $this->getTermByName('tag3');
// Login as administrator and go to admin/content.
$this->drupalGet('admin/content');
$this->assertSession()->pageTextContains('Test Article 1');
$this->assertSession()->pageTextContains('Test Article 2');
$this->assertSession()->pageTextContains('Test Page 1');
// Let's edit all three of them.
$this->submitForm([
'node_bulk_form[0]' => TRUE,
'node_bulk_form[1]' => TRUE,
'node_bulk_form[2]' => TRUE,
'action' => 'node_edit_terms_action',
], 'Apply to selected items');
$this->assertSession()->fieldExists('field_tags');
$this->assertSession()->fieldExists('field_category');
// Don't submit anything for the single-value category field, but confirm
// that "add" is not an option for it.
$this->submitForm([
'field_tags' => "{$tag1->label()} ({$tag1->id()})",
'field_tags_action' => 'remove',
'field_category_action' => 'none',
], 'Update terms');
$this->assertSession()->pageTextContains('1 node was updated.');
// Confirm the tag updates stuck. Reload entities from storage so they
// get the updates.
$article1 = $this->getNodeByTitle('Test Article 1', TRUE);
$article2 = $this->getNodeByTitle('Test Article 2', TRUE);
$page1 = $this->getNodeByTitle('Test Page 1', TRUE);
$this->assertFalse($this->hasTaxonomyTermInReferenceField($article1, 'field_tags', (int) $tag1->id()));
$this->assertFalse($this->hasTaxonomyTermInReferenceField($article1, 'field_tags', (int) $tag2->id()));
$this->assertFalse($this->hasTaxonomyTermInReferenceField($article1, 'field_tags', (int) $tag3->id()));
$this->assertFalse($this->hasTaxonomyTermInReferenceField($article2, 'field_tags', (int) $tag1->id()));
$this->assertFalse($this->hasTaxonomyTermInReferenceField($article2, 'field_tags', (int) $tag2->id()));
$this->assertTrue($this->hasTaxonomyTermInReferenceField($article2, 'field_tags', (int) $tag3->id()));
$this->assertFalse($this->hasTaxonomyTermInReferenceField($page1, 'field_tags', (int) $tag1->id()));
$this->assertFalse($this->hasTaxonomyTermInReferenceField($page1, 'field_tags', (int) $tag2->id()));
$this->assertFalse($this->hasTaxonomyTermInReferenceField($page1, 'field_tags', (int) $tag3->id()));
}
/**
* Test making a change without creating a new revision.
*/
public function testChangeWithNoRevision(): void {
// Edit all three pages.
$this->drupalGet('admin/content');
$this->submitForm([
'node_bulk_form[0]' => TRUE,
'node_bulk_form[1]' => TRUE,
'node_bulk_form[2]' => TRUE,
'action' => 'node_edit_terms_action',
], 'Apply to selected items');
// Submit a change but indicate no revision should be made.
$this->submitForm([
'field_tags_action' => 'clear',
'field_category_action' => 'clear',
'create_revision' => FALSE,
], 'Update terms');
// Only Test Article 2 should have been updated, it's the only one that
// had something to clear.
$this->assertSession()->pageTextContains('1 node was updated.');
// Verify there's only one revision still for the node.
$article2 = $this->getNodeByTitle('Test Article 2', TRUE);
$this->drupalGet('/node/' . $article2->id() . '/revisions');
$this->assertSession()->pageTextContains('Revisions for Test Article 2');
$this->assertSession()->elementsCount('css', '.node-revision-table tbody tr', 1);
}
/**
* Test making a change and providing a revision log message.
*/
public function testChangeWithRevision(): void {
// Edit all three pages.
$this->drupalGet('admin/content');
$this->submitForm([
'node_bulk_form[0]' => TRUE,
'node_bulk_form[1]' => TRUE,
'node_bulk_form[2]' => TRUE,
'action' => 'node_edit_terms_action',
], 'Apply to selected items');
// Submit a change but indicate a revision should be made.
$this->submitForm([
'field_tags_action' => 'clear',
'field_category_action' => 'clear',
'create_revision' => TRUE,
'revision_log_message' => 'Test revision log',
], 'Update terms');
// Only Test Article 2 should have been updated, it's the only one that
// had something to clear.
$this->assertSession()->pageTextContains('1 node was updated.');
// Verify that there's a new revision for the updated article.
$article2 = $this->getNodeByTitle('Test Article 2', TRUE);
$this->drupalGet('/node/' . $article2->id() . '/revisions');
$this->assertSession()->pageTextContains('Revisions for Test Article 2');
$this->assertSession()->elementsCount('css', '.node-revision-table tbody tr', 2);
$this->assertSession()->elementContains('css', '.node-revision-table tbody tr:nth-child(1)', 'Test revision log');
}
/**
* Tests using config form to change multi value form widget.
*/
public function testChangingMultivalueFormWidget(): void {
// First check using autocomplete widget (this is the default anyway).
$this->drupalGet('admin/config/content/bulk_edit_terms');
$this->submitForm([
'multi_value_widget_type' => 'entity_autocomplete',
], 'Save configuration');
$this->assertSession()->pageTextContains('The configuration options have been saved.');
// Edit all three pages.
$this->drupalGet('admin/content');
$this->submitForm([
'node_bulk_form[0]' => TRUE,
'node_bulk_form[1]' => TRUE,
'node_bulk_form[2]' => TRUE,
'action' => 'node_edit_terms_action',
], 'Apply to selected items');
// Verify that the autocomplete widget is rendered for the multi-value tags
// field.
$this->assertSession()->elementExists('css', 'input#edit-field-tags.form-autocomplete');
$this->assertSession()->elementNotExists('css', 'select#edit-field-tags');
// Change widget to select and try again.
$this->drupalGet('admin/config/content/bulk_edit_terms');
$this->submitForm([
'multi_value_widget_type' => 'select',
], 'Save configuration');
$this->assertSession()->pageTextContains('The configuration options have been saved.');
$this->drupalGet('admin/content');
$this->submitForm([
'node_bulk_form[0]' => TRUE,
'node_bulk_form[1]' => TRUE,
'node_bulk_form[2]' => TRUE,
'action' => 'node_edit_terms_action',
], 'Apply to selected items');
$this->assertSession()->elementNotExists('css', 'input#edit-field-tags.form-autocomplete');
$this->assertSession()->elementExists('css', 'select#edit-field-tags');
// Submit a change using the multi-value select and verify it works as
// expected.
$tag1 = $this->getTermByName('tag1');
$this->submitForm([
'field_tags[]' => [$tag1->id()],
'field_tags_action' => 'remove',
'field_category_action' => 'none',
], 'Update terms');
// Article 2 was the only one with tag1 on it, so it should be the only
// one updated.
$this->assertSession()->pageTextContains('1 node was updated.');
// Visit article 2 and confirm tag1 is no longer there.
$article2 = $this->getNodeByTitle('Test Article 2', TRUE);
$this->drupalGet('/node/' . $article2->id());
$this->assertSession()->pageTextNotContains('tag1');
// tag3 should still be here, we didn't mess with it.
$this->assertSession()->pageTextContains('tag3');
}
/**
* Tests users can't edit nodes or fields they don't have access to.
*/
public function testAccessRestrictions(): void {
// Create a user that only has permission to edit articles but not pages.
$limited_user = $this->drupalCreateUser([
'access content overview',
'administer nodes',
'edit any article content',
]);
$this->drupalLogin($limited_user);
$this->drupalGet('admin/content');
// Confirm when selecting a Page node, we get an error indicating we cannot
// edit it since we don't have access.
$this->submitForm([
'node_bulk_form[0]' => TRUE,
'action' => 'node_edit_terms_action',
], 'Apply to selected items');
$this->assertSession()->pageTextContains('No access to execute Update taxonomy terms on the Content Test Page 1.');
// But we are able to bulk edit an Article, which we do have perms for.
$this->submitForm([
'node_bulk_form[1]' => TRUE,
'action' => 'node_edit_terms_action',
], 'Apply to selected items');
$this->assertSession()->pageTextNotContains('No access to execute Update taxonomy terms on the Content Test Article 2.');
$this->assertSession()->pageTextNotContains('No access');
// Now let check field access perms are working.
// Start by confirming user can edit Tags field on an Article.
$this->drupalGet('admin/content');
$this->submitForm([
'node_bulk_form[1]' => TRUE,
'action' => 'node_edit_terms_action',
], 'Apply to selected items');
$this->assertSession()->elementExists('css', 'input#edit-field-tags');
// And now confirm they don't once we restrict access with a hook from
// a test module.
\Drupal::service('module_installer')->install(['bulk_edit_terms_field_permissions_test']);
$this->drupalGet('admin/content');
$this->submitForm([
'node_bulk_form[1]' => TRUE,
'action' => 'node_edit_terms_action',
], 'Apply to selected items');
$this->assertSession()->elementNotExists('css', 'input#edit-field-tags');
}
/**
* Check if an entity reference field contains a reference to a term.
*
* @param \Drupal\node\NodeInterface $node
* The node.
* @param string $fieldName
* The term reference field name.
* @param int $tid
* The term ID.
*
* @return bool
* TRUE if the term is in the reference field.
*/
private function hasTaxonomyTermInReferenceField(NodeInterface $node, string $fieldName, int $tid): bool {
$fieldItemList = $node->get($fieldName);
assert($fieldItemList instanceof EntityReferenceFieldItemListInterface);
foreach ($fieldItemList->referencedEntities() as $term) {
assert($term instanceof TermInterface);
if ((int) $term->id() === $tid) {
return TRUE;
}
}
return FALSE;
}
/**
* Get taxonomy term by name.
*
* @param string $name
* The term name.
*
* @return \Drupal\taxonomy\TermInterface
* The term.
*/
private function getTermByName(string $name): TermInterface {
$terms = \Drupal::entityTypeManager()->getStorage('taxonomy_term')->loadByProperties([
'name' => $name,
]);
return array_shift($terms);
}
}
