Initial Drupal 11 with DDEV setup
This commit is contained in:
		@ -0,0 +1,8 @@
 | 
			
		||||
name: 'Content moderation test local task'
 | 
			
		||||
type: module
 | 
			
		||||
description: 'Provides a local task for testing.'
 | 
			
		||||
package: Testing
 | 
			
		||||
version: VERSION
 | 
			
		||||
dependencies:
 | 
			
		||||
  - drupal:content_moderation
 | 
			
		||||
  - drupal:node
 | 
			
		||||
@ -0,0 +1,4 @@
 | 
			
		||||
entity.node.test_local_task_without_upcast_node:
 | 
			
		||||
  route_name: entity.node.test_local_task_without_upcast_node
 | 
			
		||||
  base_route: entity.node.canonical
 | 
			
		||||
  title: 'Task Without Upcast Node'
 | 
			
		||||
@ -0,0 +1,7 @@
 | 
			
		||||
entity.node.test_local_task_without_upcast_node:
 | 
			
		||||
  path: '/node/{node}/task-without-upcast-node'
 | 
			
		||||
  defaults:
 | 
			
		||||
    _title: 'Page Without Upcast Node'
 | 
			
		||||
    _controller: '\Drupal\content_moderation_test_local_task\Controller\TestLocalTaskController::methodWithoutUpcastNode'
 | 
			
		||||
  requirements:
 | 
			
		||||
    _access: 'TRUE'
 | 
			
		||||
@ -0,0 +1,19 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\content_moderation_test_local_task\Controller;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A test controller.
 | 
			
		||||
 */
 | 
			
		||||
class TestLocalTaskController {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * A method which does not hint the node parameter to avoid upcasting.
 | 
			
		||||
   */
 | 
			
		||||
  public function methodWithoutUpcastNode($node) {
 | 
			
		||||
    return ['#markup' => 'It works!'];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,7 @@
 | 
			
		||||
name: 'Content moderation test re-save'
 | 
			
		||||
type: module
 | 
			
		||||
description: 'Re-saves moderated entities for testing purposes.'
 | 
			
		||||
package: Testing
 | 
			
		||||
version: VERSION
 | 
			
		||||
dependencies:
 | 
			
		||||
  - drupal:content_moderation
 | 
			
		||||
@ -0,0 +1,17 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @file
 | 
			
		||||
 * Contains install functions for the Content moderation test re-save module.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Implements hook_install().
 | 
			
		||||
 */
 | 
			
		||||
function content_moderation_test_resave_install(): void {
 | 
			
		||||
  // Make sure that this module's hooks are run before Content Moderation's
 | 
			
		||||
  // hooks.
 | 
			
		||||
  module_set_weight('content_moderation_test_resave', -10);
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,38 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\content_moderation_test_resave\Hook;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Entity\EntityInterface;
 | 
			
		||||
use Drupal\Core\Hook\Attribute\Hook;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Hook implementations for content_moderation_test_resave.
 | 
			
		||||
 */
 | 
			
		||||
class ContentModerationTestResaveHooks {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Implements hook_entity_insert().
 | 
			
		||||
   */
 | 
			
		||||
  #[Hook('entity_insert')]
 | 
			
		||||
  public function entityInsert(EntityInterface $entity): void {
 | 
			
		||||
    /** @var \Drupal\content_moderation\ModerationInformationInterface $content_moderation */
 | 
			
		||||
    $content_moderation = \Drupal::service('content_moderation.moderation_information');
 | 
			
		||||
    if ($content_moderation->isModeratedEntity($entity)) {
 | 
			
		||||
      /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
 | 
			
		||||
      // Saving the passed entity object would populate its loaded revision ID,
 | 
			
		||||
      // which we want to avoid. Thus, save a clone of the original object.
 | 
			
		||||
      $cloned_entity = clone $entity;
 | 
			
		||||
      // Set the entity's syncing status, as we do not want Content Moderation
 | 
			
		||||
      // to create new revisions for the re-saving. Without this call Content
 | 
			
		||||
      // Moderation would end up creating two separate content moderation state
 | 
			
		||||
      // entities: one for the re-save revision and one for the initial
 | 
			
		||||
      // revision.
 | 
			
		||||
      $cloned_entity->setSyncing(TRUE)->save();
 | 
			
		||||
      // Record the fact that a re-save happened.
 | 
			
		||||
      \Drupal::state()->set('content_moderation_test_resave', TRUE);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,404 @@
 | 
			
		||||
langcode: en
 | 
			
		||||
status: true
 | 
			
		||||
dependencies:
 | 
			
		||||
  config:
 | 
			
		||||
    - system.menu.main
 | 
			
		||||
  module:
 | 
			
		||||
    - content_moderation
 | 
			
		||||
    - node
 | 
			
		||||
    - user
 | 
			
		||||
id: latest
 | 
			
		||||
label: Latest
 | 
			
		||||
module: views
 | 
			
		||||
description: ''
 | 
			
		||||
tag: ''
 | 
			
		||||
base_table: node_field_revision
 | 
			
		||||
base_field: vid
 | 
			
		||||
display:
 | 
			
		||||
  default:
 | 
			
		||||
    id: default
 | 
			
		||||
    display_title: Default
 | 
			
		||||
    display_plugin: default
 | 
			
		||||
    position: 0
 | 
			
		||||
    display_options:
 | 
			
		||||
      title: Latest
 | 
			
		||||
      fields:
 | 
			
		||||
        nid:
 | 
			
		||||
          id: nid
 | 
			
		||||
          table: node_field_revision
 | 
			
		||||
          field: nid
 | 
			
		||||
          relationship: none
 | 
			
		||||
          group_type: group
 | 
			
		||||
          admin_label: ''
 | 
			
		||||
          entity_type: node
 | 
			
		||||
          entity_field: nid
 | 
			
		||||
          plugin_id: field
 | 
			
		||||
          label: 'Node ID'
 | 
			
		||||
          exclude: false
 | 
			
		||||
          alter:
 | 
			
		||||
            alter_text: false
 | 
			
		||||
            text: ''
 | 
			
		||||
            make_link: false
 | 
			
		||||
            path: ''
 | 
			
		||||
            absolute: false
 | 
			
		||||
            external: false
 | 
			
		||||
            replace_spaces: false
 | 
			
		||||
            path_case: none
 | 
			
		||||
            trim_whitespace: false
 | 
			
		||||
            alt: ''
 | 
			
		||||
            rel: ''
 | 
			
		||||
            link_class: ''
 | 
			
		||||
            prefix: ''
 | 
			
		||||
            suffix: ''
 | 
			
		||||
            target: ''
 | 
			
		||||
            nl2br: false
 | 
			
		||||
            max_length: 0
 | 
			
		||||
            word_boundary: true
 | 
			
		||||
            ellipsis: true
 | 
			
		||||
            more_link: false
 | 
			
		||||
            more_link_text: ''
 | 
			
		||||
            more_link_path: ''
 | 
			
		||||
            strip_tags: false
 | 
			
		||||
            trim: false
 | 
			
		||||
            preserve_tags: ''
 | 
			
		||||
            html: false
 | 
			
		||||
          element_type: ''
 | 
			
		||||
          element_class: ''
 | 
			
		||||
          element_label_type: ''
 | 
			
		||||
          element_label_class: ''
 | 
			
		||||
          element_label_colon: true
 | 
			
		||||
          element_wrapper_type: ''
 | 
			
		||||
          element_wrapper_class: ''
 | 
			
		||||
          element_default_classes: true
 | 
			
		||||
          empty: ''
 | 
			
		||||
          hide_empty: false
 | 
			
		||||
          empty_zero: false
 | 
			
		||||
          hide_alter_empty: true
 | 
			
		||||
          click_sort_column: value
 | 
			
		||||
          type: number_integer
 | 
			
		||||
          settings:
 | 
			
		||||
            thousand_separator: ''
 | 
			
		||||
            prefix_suffix: true
 | 
			
		||||
          group_column: value
 | 
			
		||||
          group_columns: {  }
 | 
			
		||||
          group_rows: true
 | 
			
		||||
          delta_limit: 0
 | 
			
		||||
          delta_offset: 0
 | 
			
		||||
          delta_reversed: false
 | 
			
		||||
          delta_first_last: false
 | 
			
		||||
          multi_type: separator
 | 
			
		||||
          separator: ', '
 | 
			
		||||
          field_api_classes: false
 | 
			
		||||
        vid:
 | 
			
		||||
          id: vid
 | 
			
		||||
          table: node_field_revision
 | 
			
		||||
          field: vid
 | 
			
		||||
          relationship: none
 | 
			
		||||
          group_type: group
 | 
			
		||||
          admin_label: ''
 | 
			
		||||
          entity_type: node
 | 
			
		||||
          entity_field: vid
 | 
			
		||||
          plugin_id: field
 | 
			
		||||
          label: 'Revision ID'
 | 
			
		||||
          exclude: false
 | 
			
		||||
          alter:
 | 
			
		||||
            alter_text: false
 | 
			
		||||
            text: ''
 | 
			
		||||
            make_link: false
 | 
			
		||||
            path: ''
 | 
			
		||||
            absolute: false
 | 
			
		||||
            external: false
 | 
			
		||||
            replace_spaces: false
 | 
			
		||||
            path_case: none
 | 
			
		||||
            trim_whitespace: false
 | 
			
		||||
            alt: ''
 | 
			
		||||
            rel: ''
 | 
			
		||||
            link_class: ''
 | 
			
		||||
            prefix: ''
 | 
			
		||||
            suffix: ''
 | 
			
		||||
            target: ''
 | 
			
		||||
            nl2br: false
 | 
			
		||||
            max_length: 0
 | 
			
		||||
            word_boundary: true
 | 
			
		||||
            ellipsis: true
 | 
			
		||||
            more_link: false
 | 
			
		||||
            more_link_text: ''
 | 
			
		||||
            more_link_path: ''
 | 
			
		||||
            strip_tags: false
 | 
			
		||||
            trim: false
 | 
			
		||||
            preserve_tags: ''
 | 
			
		||||
            html: false
 | 
			
		||||
          element_type: ''
 | 
			
		||||
          element_class: ''
 | 
			
		||||
          element_label_type: ''
 | 
			
		||||
          element_label_class: ''
 | 
			
		||||
          element_label_colon: true
 | 
			
		||||
          element_wrapper_type: ''
 | 
			
		||||
          element_wrapper_class: ''
 | 
			
		||||
          element_default_classes: true
 | 
			
		||||
          empty: ''
 | 
			
		||||
          hide_empty: false
 | 
			
		||||
          empty_zero: false
 | 
			
		||||
          hide_alter_empty: true
 | 
			
		||||
          click_sort_column: value
 | 
			
		||||
          type: number_integer
 | 
			
		||||
          settings:
 | 
			
		||||
            thousand_separator: ''
 | 
			
		||||
            prefix_suffix: true
 | 
			
		||||
          group_column: value
 | 
			
		||||
          group_columns: {  }
 | 
			
		||||
          group_rows: true
 | 
			
		||||
          delta_limit: 0
 | 
			
		||||
          delta_offset: 0
 | 
			
		||||
          delta_reversed: false
 | 
			
		||||
          delta_first_last: false
 | 
			
		||||
          multi_type: separator
 | 
			
		||||
          separator: ', '
 | 
			
		||||
          field_api_classes: false
 | 
			
		||||
        title:
 | 
			
		||||
          id: title
 | 
			
		||||
          table: node_field_revision
 | 
			
		||||
          field: title
 | 
			
		||||
          relationship: none
 | 
			
		||||
          group_type: group
 | 
			
		||||
          admin_label: ''
 | 
			
		||||
          entity_type: node
 | 
			
		||||
          entity_field: title
 | 
			
		||||
          plugin_id: field
 | 
			
		||||
          label: Title
 | 
			
		||||
          exclude: false
 | 
			
		||||
          alter:
 | 
			
		||||
            alter_text: false
 | 
			
		||||
            make_link: false
 | 
			
		||||
            absolute: false
 | 
			
		||||
            word_boundary: false
 | 
			
		||||
            ellipsis: false
 | 
			
		||||
            strip_tags: false
 | 
			
		||||
            trim: false
 | 
			
		||||
            html: false
 | 
			
		||||
          element_type: ''
 | 
			
		||||
          element_class: ''
 | 
			
		||||
          element_label_type: ''
 | 
			
		||||
          element_label_class: ''
 | 
			
		||||
          element_label_colon: true
 | 
			
		||||
          element_wrapper_type: ''
 | 
			
		||||
          element_wrapper_class: ''
 | 
			
		||||
          element_default_classes: true
 | 
			
		||||
          empty: ''
 | 
			
		||||
          hide_empty: false
 | 
			
		||||
          empty_zero: false
 | 
			
		||||
          hide_alter_empty: true
 | 
			
		||||
          click_sort_column: value
 | 
			
		||||
          type: string
 | 
			
		||||
          settings:
 | 
			
		||||
            link_to_entity: false
 | 
			
		||||
          group_column: value
 | 
			
		||||
          group_columns: {  }
 | 
			
		||||
          group_rows: true
 | 
			
		||||
          delta_limit: 0
 | 
			
		||||
          delta_offset: 0
 | 
			
		||||
          delta_reversed: false
 | 
			
		||||
          delta_first_last: false
 | 
			
		||||
          multi_type: separator
 | 
			
		||||
          separator: ', '
 | 
			
		||||
          field_api_classes: false
 | 
			
		||||
        moderation_state_1:
 | 
			
		||||
          id: moderation_state_1
 | 
			
		||||
          table: node_field_revision
 | 
			
		||||
          field: moderation_state
 | 
			
		||||
          relationship: none
 | 
			
		||||
          group_type: group
 | 
			
		||||
          admin_label: ''
 | 
			
		||||
          entity_type: node
 | 
			
		||||
          plugin_id: moderation_state_field
 | 
			
		||||
          label: 'Moderation state'
 | 
			
		||||
          exclude: false
 | 
			
		||||
          alter:
 | 
			
		||||
            alter_text: false
 | 
			
		||||
            text: ''
 | 
			
		||||
            make_link: false
 | 
			
		||||
            path: ''
 | 
			
		||||
            absolute: false
 | 
			
		||||
            external: false
 | 
			
		||||
            replace_spaces: false
 | 
			
		||||
            path_case: none
 | 
			
		||||
            trim_whitespace: false
 | 
			
		||||
            alt: ''
 | 
			
		||||
            rel: ''
 | 
			
		||||
            link_class: ''
 | 
			
		||||
            prefix: ''
 | 
			
		||||
            suffix: ''
 | 
			
		||||
            target: ''
 | 
			
		||||
            nl2br: false
 | 
			
		||||
            max_length: 0
 | 
			
		||||
            word_boundary: true
 | 
			
		||||
            ellipsis: true
 | 
			
		||||
            more_link: false
 | 
			
		||||
            more_link_text: ''
 | 
			
		||||
            more_link_path: ''
 | 
			
		||||
            strip_tags: false
 | 
			
		||||
            trim: false
 | 
			
		||||
            preserve_tags: ''
 | 
			
		||||
            html: false
 | 
			
		||||
          element_type: ''
 | 
			
		||||
          element_class: ''
 | 
			
		||||
          element_label_type: ''
 | 
			
		||||
          element_label_class: ''
 | 
			
		||||
          element_label_colon: true
 | 
			
		||||
          element_wrapper_type: ''
 | 
			
		||||
          element_wrapper_class: ''
 | 
			
		||||
          element_default_classes: true
 | 
			
		||||
          empty: ''
 | 
			
		||||
          hide_empty: false
 | 
			
		||||
          empty_zero: false
 | 
			
		||||
          hide_alter_empty: true
 | 
			
		||||
          click_sort_column: value
 | 
			
		||||
          type: content_moderation_state
 | 
			
		||||
          settings: {  }
 | 
			
		||||
          group_column: value
 | 
			
		||||
          group_columns: {  }
 | 
			
		||||
          group_rows: true
 | 
			
		||||
          delta_limit: 0
 | 
			
		||||
          delta_offset: 0
 | 
			
		||||
          delta_reversed: false
 | 
			
		||||
          delta_first_last: false
 | 
			
		||||
          multi_type: separator
 | 
			
		||||
          separator: ', '
 | 
			
		||||
          field_api_classes: false
 | 
			
		||||
      pager:
 | 
			
		||||
        type: full
 | 
			
		||||
        options:
 | 
			
		||||
          offset: 0
 | 
			
		||||
          items_per_page: 10
 | 
			
		||||
          total_pages: null
 | 
			
		||||
          id: 0
 | 
			
		||||
          tags:
 | 
			
		||||
            next: 'Next ›'
 | 
			
		||||
            previous: '‹ Previous'
 | 
			
		||||
            first: '« First'
 | 
			
		||||
            last: 'Last »'
 | 
			
		||||
          expose:
 | 
			
		||||
            items_per_page: false
 | 
			
		||||
            items_per_page_label: 'Items per page'
 | 
			
		||||
            items_per_page_options: '5, 10, 25, 50'
 | 
			
		||||
            items_per_page_options_all: false
 | 
			
		||||
            items_per_page_options_all_label: '- All -'
 | 
			
		||||
            offset: false
 | 
			
		||||
            offset_label: Offset
 | 
			
		||||
          quantity: 9
 | 
			
		||||
      exposed_form:
 | 
			
		||||
        type: basic
 | 
			
		||||
        options:
 | 
			
		||||
          submit_button: Apply
 | 
			
		||||
          reset_button: false
 | 
			
		||||
          reset_button_label: Reset
 | 
			
		||||
          exposed_sorts_label: 'Sort by'
 | 
			
		||||
          expose_sort_order: true
 | 
			
		||||
          sort_asc_label: Asc
 | 
			
		||||
          sort_desc_label: Desc
 | 
			
		||||
      access:
 | 
			
		||||
        type: perm
 | 
			
		||||
        options:
 | 
			
		||||
          perm: 'view all revisions'
 | 
			
		||||
      cache:
 | 
			
		||||
        type: tag
 | 
			
		||||
        options: {  }
 | 
			
		||||
      empty: {  }
 | 
			
		||||
      sorts: {  }
 | 
			
		||||
      arguments: {  }
 | 
			
		||||
      filters:
 | 
			
		||||
        latest_revision:
 | 
			
		||||
          id: latest_revision
 | 
			
		||||
          table: node_revision
 | 
			
		||||
          field: latest_revision
 | 
			
		||||
          relationship: none
 | 
			
		||||
          group_type: group
 | 
			
		||||
          admin_label: ''
 | 
			
		||||
          entity_type: node
 | 
			
		||||
          plugin_id: latest_revision
 | 
			
		||||
          operator: '='
 | 
			
		||||
          value: ''
 | 
			
		||||
          group: 1
 | 
			
		||||
          exposed: false
 | 
			
		||||
          expose:
 | 
			
		||||
            operator_id: ''
 | 
			
		||||
            label: ''
 | 
			
		||||
            description: ''
 | 
			
		||||
            use_operator: false
 | 
			
		||||
            operator: ''
 | 
			
		||||
            operator_limit_selection: false
 | 
			
		||||
            operator_list: {  }
 | 
			
		||||
            identifier: ''
 | 
			
		||||
            required: false
 | 
			
		||||
            remember: false
 | 
			
		||||
            multiple: false
 | 
			
		||||
            remember_roles:
 | 
			
		||||
              authenticated: authenticated
 | 
			
		||||
          is_grouped: false
 | 
			
		||||
          group_info:
 | 
			
		||||
            label: ''
 | 
			
		||||
            description: ''
 | 
			
		||||
            identifier: ''
 | 
			
		||||
            optional: true
 | 
			
		||||
            widget: select
 | 
			
		||||
            multiple: false
 | 
			
		||||
            remember: false
 | 
			
		||||
            default_group: All
 | 
			
		||||
            default_group_multiple: {  }
 | 
			
		||||
            group_items: {  }
 | 
			
		||||
      style:
 | 
			
		||||
        type: table
 | 
			
		||||
        options:
 | 
			
		||||
          grouping: {  }
 | 
			
		||||
          class: ''
 | 
			
		||||
          row_class: ''
 | 
			
		||||
          default_row_class: true
 | 
			
		||||
      row:
 | 
			
		||||
        type: fields
 | 
			
		||||
      query:
 | 
			
		||||
        type: views_query
 | 
			
		||||
        options:
 | 
			
		||||
          query_comment: ''
 | 
			
		||||
          disable_sql_rewrite: false
 | 
			
		||||
          distinct: false
 | 
			
		||||
          replica: false
 | 
			
		||||
          query_tags: {  }
 | 
			
		||||
      relationships: {  }
 | 
			
		||||
      header: {  }
 | 
			
		||||
      footer: {  }
 | 
			
		||||
      display_extenders: {  }
 | 
			
		||||
    cache_metadata:
 | 
			
		||||
      max-age: -1
 | 
			
		||||
      contexts:
 | 
			
		||||
        - 'languages:language_content'
 | 
			
		||||
        - 'languages:language_interface'
 | 
			
		||||
        - url.query_args
 | 
			
		||||
        - 'user.node_grants:view'
 | 
			
		||||
        - user.permissions
 | 
			
		||||
      tags: {  }
 | 
			
		||||
  page_1:
 | 
			
		||||
    id: page_1
 | 
			
		||||
    display_title: Page
 | 
			
		||||
    display_plugin: page
 | 
			
		||||
    position: 1
 | 
			
		||||
    display_options:
 | 
			
		||||
      display_extenders: {  }
 | 
			
		||||
      path: latest
 | 
			
		||||
      menu:
 | 
			
		||||
        type: normal
 | 
			
		||||
        title: Drafts
 | 
			
		||||
        description: ''
 | 
			
		||||
        weight: 0
 | 
			
		||||
        expanded: false
 | 
			
		||||
        menu_name: main
 | 
			
		||||
        parent: ''
 | 
			
		||||
        context: '0'
 | 
			
		||||
    cache_metadata:
 | 
			
		||||
      max-age: -1
 | 
			
		||||
      contexts:
 | 
			
		||||
        - 'languages:language_content'
 | 
			
		||||
        - 'languages:language_interface'
 | 
			
		||||
        - url.query_args
 | 
			
		||||
        - 'user.node_grants:view'
 | 
			
		||||
        - user.permissions
 | 
			
		||||
      tags: {  }
 | 
			
		||||
@ -0,0 +1,204 @@
 | 
			
		||||
langcode: en
 | 
			
		||||
status: true
 | 
			
		||||
dependencies:
 | 
			
		||||
  module:
 | 
			
		||||
    - content_moderation
 | 
			
		||||
    - node
 | 
			
		||||
    - user
 | 
			
		||||
id: test_content_moderation_field_state_test
 | 
			
		||||
label: test_content_moderation_field_state_test
 | 
			
		||||
module: views
 | 
			
		||||
description: ''
 | 
			
		||||
tag: ''
 | 
			
		||||
base_table: node_field_data
 | 
			
		||||
base_field: nid
 | 
			
		||||
display:
 | 
			
		||||
  default:
 | 
			
		||||
    id: default
 | 
			
		||||
    display_title: Default
 | 
			
		||||
    display_plugin: default
 | 
			
		||||
    position: 0
 | 
			
		||||
    display_options:
 | 
			
		||||
      title: test_content_moderation_field_state_test
 | 
			
		||||
      fields:
 | 
			
		||||
        title:
 | 
			
		||||
          id: title
 | 
			
		||||
          table: node_field_data
 | 
			
		||||
          field: title
 | 
			
		||||
          relationship: none
 | 
			
		||||
          group_type: group
 | 
			
		||||
          admin_label: ''
 | 
			
		||||
          entity_type: node
 | 
			
		||||
          entity_field: title
 | 
			
		||||
          plugin_id: field
 | 
			
		||||
          label: ''
 | 
			
		||||
          exclude: false
 | 
			
		||||
          alter:
 | 
			
		||||
            alter_text: false
 | 
			
		||||
            make_link: false
 | 
			
		||||
            absolute: false
 | 
			
		||||
            word_boundary: false
 | 
			
		||||
            ellipsis: false
 | 
			
		||||
            strip_tags: false
 | 
			
		||||
            trim: false
 | 
			
		||||
            html: false
 | 
			
		||||
          element_type: ''
 | 
			
		||||
          element_class: ''
 | 
			
		||||
          element_label_type: ''
 | 
			
		||||
          element_label_class: ''
 | 
			
		||||
          element_label_colon: true
 | 
			
		||||
          element_wrapper_type: ''
 | 
			
		||||
          element_wrapper_class: ''
 | 
			
		||||
          element_default_classes: true
 | 
			
		||||
          empty: ''
 | 
			
		||||
          hide_empty: false
 | 
			
		||||
          empty_zero: false
 | 
			
		||||
          hide_alter_empty: true
 | 
			
		||||
          click_sort_column: value
 | 
			
		||||
          type: string
 | 
			
		||||
          settings:
 | 
			
		||||
            link_to_entity: true
 | 
			
		||||
          group_column: value
 | 
			
		||||
          group_columns: {  }
 | 
			
		||||
          group_rows: true
 | 
			
		||||
          delta_limit: 0
 | 
			
		||||
          delta_offset: 0
 | 
			
		||||
          delta_reversed: false
 | 
			
		||||
          delta_first_last: false
 | 
			
		||||
          multi_type: separator
 | 
			
		||||
          separator: ', '
 | 
			
		||||
          field_api_classes: false
 | 
			
		||||
        moderation_state:
 | 
			
		||||
          id: moderation_state
 | 
			
		||||
          table: node_field_data
 | 
			
		||||
          field: moderation_state
 | 
			
		||||
          relationship: none
 | 
			
		||||
          group_type: group
 | 
			
		||||
          admin_label: ''
 | 
			
		||||
          entity_type: node
 | 
			
		||||
          plugin_id: field
 | 
			
		||||
          label: ''
 | 
			
		||||
          exclude: false
 | 
			
		||||
          alter:
 | 
			
		||||
            alter_text: false
 | 
			
		||||
            text: ''
 | 
			
		||||
            make_link: false
 | 
			
		||||
            path: ''
 | 
			
		||||
            absolute: false
 | 
			
		||||
            external: false
 | 
			
		||||
            replace_spaces: false
 | 
			
		||||
            path_case: none
 | 
			
		||||
            trim_whitespace: false
 | 
			
		||||
            alt: ''
 | 
			
		||||
            rel: ''
 | 
			
		||||
            link_class: ''
 | 
			
		||||
            prefix: ''
 | 
			
		||||
            suffix: ''
 | 
			
		||||
            target: ''
 | 
			
		||||
            nl2br: false
 | 
			
		||||
            max_length: 0
 | 
			
		||||
            word_boundary: true
 | 
			
		||||
            ellipsis: true
 | 
			
		||||
            more_link: false
 | 
			
		||||
            more_link_text: ''
 | 
			
		||||
            more_link_path: ''
 | 
			
		||||
            strip_tags: false
 | 
			
		||||
            trim: false
 | 
			
		||||
            preserve_tags: ''
 | 
			
		||||
            html: false
 | 
			
		||||
          element_type: ''
 | 
			
		||||
          element_class: ''
 | 
			
		||||
          element_label_type: ''
 | 
			
		||||
          element_label_class: ''
 | 
			
		||||
          element_label_colon: false
 | 
			
		||||
          element_wrapper_type: ''
 | 
			
		||||
          element_wrapper_class: ''
 | 
			
		||||
          element_default_classes: true
 | 
			
		||||
          empty: ''
 | 
			
		||||
          hide_empty: false
 | 
			
		||||
          empty_zero: false
 | 
			
		||||
          hide_alter_empty: true
 | 
			
		||||
          click_sort_column: value
 | 
			
		||||
          type: content_moderation_state
 | 
			
		||||
          settings: {  }
 | 
			
		||||
          group_column: value
 | 
			
		||||
          group_columns: {  }
 | 
			
		||||
          group_rows: true
 | 
			
		||||
          delta_limit: 0
 | 
			
		||||
          delta_offset: 0
 | 
			
		||||
          delta_reversed: false
 | 
			
		||||
          delta_first_last: false
 | 
			
		||||
          multi_type: separator
 | 
			
		||||
          separator: ', '
 | 
			
		||||
          field_api_classes: false
 | 
			
		||||
      pager:
 | 
			
		||||
        type: some
 | 
			
		||||
        options:
 | 
			
		||||
          offset: 0
 | 
			
		||||
          items_per_page: 10
 | 
			
		||||
      exposed_form:
 | 
			
		||||
        type: basic
 | 
			
		||||
        options:
 | 
			
		||||
          submit_button: Apply
 | 
			
		||||
          reset_button: false
 | 
			
		||||
          reset_button_label: Reset
 | 
			
		||||
          exposed_sorts_label: 'Sort by'
 | 
			
		||||
          expose_sort_order: true
 | 
			
		||||
          sort_asc_label: Asc
 | 
			
		||||
          sort_desc_label: Desc
 | 
			
		||||
      access:
 | 
			
		||||
        type: perm
 | 
			
		||||
        options:
 | 
			
		||||
          perm: 'access content'
 | 
			
		||||
      cache:
 | 
			
		||||
        type: tag
 | 
			
		||||
        options: {  }
 | 
			
		||||
      empty: {  }
 | 
			
		||||
      sorts: {  }
 | 
			
		||||
      arguments: {  }
 | 
			
		||||
      filters: {  }
 | 
			
		||||
      style:
 | 
			
		||||
        type: default
 | 
			
		||||
      row:
 | 
			
		||||
        type: fields
 | 
			
		||||
        options:
 | 
			
		||||
          default_field_elements: true
 | 
			
		||||
          inline: {  }
 | 
			
		||||
          separator: ''
 | 
			
		||||
          hide_empty: false
 | 
			
		||||
      query:
 | 
			
		||||
        type: views_query
 | 
			
		||||
        options:
 | 
			
		||||
          query_comment: ''
 | 
			
		||||
          disable_sql_rewrite: false
 | 
			
		||||
          distinct: false
 | 
			
		||||
          replica: false
 | 
			
		||||
          query_tags: {  }
 | 
			
		||||
      relationships: {  }
 | 
			
		||||
      header: {  }
 | 
			
		||||
      footer: {  }
 | 
			
		||||
      display_extenders: {  }
 | 
			
		||||
    cache_metadata:
 | 
			
		||||
      max-age: -1
 | 
			
		||||
      contexts:
 | 
			
		||||
        - 'languages:language_content'
 | 
			
		||||
        - 'languages:language_interface'
 | 
			
		||||
        - 'user.node_grants:view'
 | 
			
		||||
        - user.permissions
 | 
			
		||||
      tags: {  }
 | 
			
		||||
  page_1:
 | 
			
		||||
    id: page_1
 | 
			
		||||
    display_title: Page
 | 
			
		||||
    display_plugin: page
 | 
			
		||||
    position: 1
 | 
			
		||||
    display_options:
 | 
			
		||||
      display_extenders: {  }
 | 
			
		||||
      path: test-content-moderation-field-state-test
 | 
			
		||||
    cache_metadata:
 | 
			
		||||
      max-age: -1
 | 
			
		||||
      contexts:
 | 
			
		||||
        - 'languages:language_content'
 | 
			
		||||
        - 'languages:language_interface'
 | 
			
		||||
        - 'user.node_grants:view'
 | 
			
		||||
        - user.permissions
 | 
			
		||||
      tags: {  }
 | 
			
		||||
@ -0,0 +1,346 @@
 | 
			
		||||
langcode: en
 | 
			
		||||
status: true
 | 
			
		||||
dependencies:
 | 
			
		||||
  module:
 | 
			
		||||
    - content_moderation
 | 
			
		||||
    - node
 | 
			
		||||
    - user
 | 
			
		||||
id: test_content_moderation_filter_via_relationship
 | 
			
		||||
label: test_content_moderation_filter_via_relationship
 | 
			
		||||
module: views
 | 
			
		||||
description: ''
 | 
			
		||||
tag: ''
 | 
			
		||||
base_table: users_field_data
 | 
			
		||||
base_field: uid
 | 
			
		||||
display:
 | 
			
		||||
  default:
 | 
			
		||||
    id: default
 | 
			
		||||
    display_title: Default
 | 
			
		||||
    display_plugin: default
 | 
			
		||||
    position: 0
 | 
			
		||||
    display_options:
 | 
			
		||||
      title: test_content_moderation_filter_via_relationship
 | 
			
		||||
      fields:
 | 
			
		||||
        name:
 | 
			
		||||
          id: name
 | 
			
		||||
          table: users_field_data
 | 
			
		||||
          field: name
 | 
			
		||||
          relationship: none
 | 
			
		||||
          group_type: group
 | 
			
		||||
          admin_label: ''
 | 
			
		||||
          entity_type: user
 | 
			
		||||
          entity_field: name
 | 
			
		||||
          plugin_id: field
 | 
			
		||||
          label: ''
 | 
			
		||||
          exclude: false
 | 
			
		||||
          alter:
 | 
			
		||||
            alter_text: false
 | 
			
		||||
            text: ''
 | 
			
		||||
            make_link: false
 | 
			
		||||
            path: ''
 | 
			
		||||
            absolute: false
 | 
			
		||||
            external: false
 | 
			
		||||
            replace_spaces: false
 | 
			
		||||
            path_case: none
 | 
			
		||||
            trim_whitespace: false
 | 
			
		||||
            alt: ''
 | 
			
		||||
            rel: ''
 | 
			
		||||
            link_class: ''
 | 
			
		||||
            prefix: ''
 | 
			
		||||
            suffix: ''
 | 
			
		||||
            target: ''
 | 
			
		||||
            nl2br: false
 | 
			
		||||
            max_length: 0
 | 
			
		||||
            word_boundary: false
 | 
			
		||||
            ellipsis: false
 | 
			
		||||
            more_link: false
 | 
			
		||||
            more_link_text: ''
 | 
			
		||||
            more_link_path: ''
 | 
			
		||||
            strip_tags: false
 | 
			
		||||
            trim: false
 | 
			
		||||
            preserve_tags: ''
 | 
			
		||||
            html: false
 | 
			
		||||
          element_type: ''
 | 
			
		||||
          element_class: ''
 | 
			
		||||
          element_label_type: ''
 | 
			
		||||
          element_label_class: ''
 | 
			
		||||
          element_label_colon: false
 | 
			
		||||
          element_wrapper_type: ''
 | 
			
		||||
          element_wrapper_class: ''
 | 
			
		||||
          element_default_classes: true
 | 
			
		||||
          empty: ''
 | 
			
		||||
          hide_empty: false
 | 
			
		||||
          empty_zero: false
 | 
			
		||||
          hide_alter_empty: true
 | 
			
		||||
          click_sort_column: value
 | 
			
		||||
          type: user_name
 | 
			
		||||
          settings:
 | 
			
		||||
            link_to_entity: false
 | 
			
		||||
          group_column: value
 | 
			
		||||
          group_columns: {  }
 | 
			
		||||
          group_rows: true
 | 
			
		||||
          delta_limit: 0
 | 
			
		||||
          delta_offset: 0
 | 
			
		||||
          delta_reversed: false
 | 
			
		||||
          delta_first_last: false
 | 
			
		||||
          multi_type: separator
 | 
			
		||||
          separator: ', '
 | 
			
		||||
          field_api_classes: false
 | 
			
		||||
        title:
 | 
			
		||||
          id: title
 | 
			
		||||
          table: node_field_data
 | 
			
		||||
          field: title
 | 
			
		||||
          relationship: uid
 | 
			
		||||
          group_type: group
 | 
			
		||||
          admin_label: ''
 | 
			
		||||
          entity_type: node
 | 
			
		||||
          entity_field: title
 | 
			
		||||
          plugin_id: field
 | 
			
		||||
          label: ''
 | 
			
		||||
          exclude: false
 | 
			
		||||
          alter:
 | 
			
		||||
            alter_text: false
 | 
			
		||||
            text: ''
 | 
			
		||||
            make_link: false
 | 
			
		||||
            path: ''
 | 
			
		||||
            absolute: false
 | 
			
		||||
            external: false
 | 
			
		||||
            replace_spaces: false
 | 
			
		||||
            path_case: none
 | 
			
		||||
            trim_whitespace: false
 | 
			
		||||
            alt: ''
 | 
			
		||||
            rel: ''
 | 
			
		||||
            link_class: ''
 | 
			
		||||
            prefix: ''
 | 
			
		||||
            suffix: ''
 | 
			
		||||
            target: ''
 | 
			
		||||
            nl2br: false
 | 
			
		||||
            max_length: 0
 | 
			
		||||
            word_boundary: true
 | 
			
		||||
            ellipsis: true
 | 
			
		||||
            more_link: false
 | 
			
		||||
            more_link_text: ''
 | 
			
		||||
            more_link_path: ''
 | 
			
		||||
            strip_tags: false
 | 
			
		||||
            trim: false
 | 
			
		||||
            preserve_tags: ''
 | 
			
		||||
            html: false
 | 
			
		||||
          element_type: ''
 | 
			
		||||
          element_class: ''
 | 
			
		||||
          element_label_type: ''
 | 
			
		||||
          element_label_class: ''
 | 
			
		||||
          element_label_colon: false
 | 
			
		||||
          element_wrapper_type: ''
 | 
			
		||||
          element_wrapper_class: ''
 | 
			
		||||
          element_default_classes: true
 | 
			
		||||
          empty: ''
 | 
			
		||||
          hide_empty: false
 | 
			
		||||
          empty_zero: false
 | 
			
		||||
          hide_alter_empty: true
 | 
			
		||||
          click_sort_column: value
 | 
			
		||||
          type: string
 | 
			
		||||
          settings:
 | 
			
		||||
            link_to_entity: false
 | 
			
		||||
          group_column: value
 | 
			
		||||
          group_columns: {  }
 | 
			
		||||
          group_rows: true
 | 
			
		||||
          delta_limit: 0
 | 
			
		||||
          delta_offset: 0
 | 
			
		||||
          delta_reversed: false
 | 
			
		||||
          delta_first_last: false
 | 
			
		||||
          multi_type: separator
 | 
			
		||||
          separator: ', '
 | 
			
		||||
          field_api_classes: false
 | 
			
		||||
        moderation_state:
 | 
			
		||||
          id: moderation_state
 | 
			
		||||
          table: node_field_data
 | 
			
		||||
          field: moderation_state
 | 
			
		||||
          relationship: uid
 | 
			
		||||
          group_type: group
 | 
			
		||||
          admin_label: ''
 | 
			
		||||
          entity_type: node
 | 
			
		||||
          plugin_id: moderation_state_field
 | 
			
		||||
          label: ''
 | 
			
		||||
          exclude: false
 | 
			
		||||
          alter:
 | 
			
		||||
            alter_text: false
 | 
			
		||||
            text: ''
 | 
			
		||||
            make_link: false
 | 
			
		||||
            path: ''
 | 
			
		||||
            absolute: false
 | 
			
		||||
            external: false
 | 
			
		||||
            replace_spaces: false
 | 
			
		||||
            path_case: none
 | 
			
		||||
            trim_whitespace: false
 | 
			
		||||
            alt: ''
 | 
			
		||||
            rel: ''
 | 
			
		||||
            link_class: ''
 | 
			
		||||
            prefix: ''
 | 
			
		||||
            suffix: ''
 | 
			
		||||
            target: ''
 | 
			
		||||
            nl2br: false
 | 
			
		||||
            max_length: 0
 | 
			
		||||
            word_boundary: true
 | 
			
		||||
            ellipsis: true
 | 
			
		||||
            more_link: false
 | 
			
		||||
            more_link_text: ''
 | 
			
		||||
            more_link_path: ''
 | 
			
		||||
            strip_tags: false
 | 
			
		||||
            trim: false
 | 
			
		||||
            preserve_tags: ''
 | 
			
		||||
            html: false
 | 
			
		||||
          element_type: ''
 | 
			
		||||
          element_class: ''
 | 
			
		||||
          element_label_type: ''
 | 
			
		||||
          element_label_class: ''
 | 
			
		||||
          element_label_colon: false
 | 
			
		||||
          element_wrapper_type: ''
 | 
			
		||||
          element_wrapper_class: ''
 | 
			
		||||
          element_default_classes: true
 | 
			
		||||
          empty: ''
 | 
			
		||||
          hide_empty: false
 | 
			
		||||
          empty_zero: false
 | 
			
		||||
          hide_alter_empty: true
 | 
			
		||||
          click_sort_column: value
 | 
			
		||||
          type: content_moderation_state
 | 
			
		||||
          settings: {  }
 | 
			
		||||
          group_column: value
 | 
			
		||||
          group_columns: {  }
 | 
			
		||||
          group_rows: true
 | 
			
		||||
          delta_limit: 0
 | 
			
		||||
          delta_offset: 0
 | 
			
		||||
          delta_reversed: false
 | 
			
		||||
          delta_first_last: false
 | 
			
		||||
          multi_type: separator
 | 
			
		||||
          separator: ', '
 | 
			
		||||
          field_api_classes: false
 | 
			
		||||
      pager:
 | 
			
		||||
        type: none
 | 
			
		||||
        options:
 | 
			
		||||
          offset: 0
 | 
			
		||||
      exposed_form:
 | 
			
		||||
        type: basic
 | 
			
		||||
        options:
 | 
			
		||||
          submit_button: Apply
 | 
			
		||||
          reset_button: false
 | 
			
		||||
          reset_button_label: Reset
 | 
			
		||||
          exposed_sorts_label: 'Sort by'
 | 
			
		||||
          expose_sort_order: true
 | 
			
		||||
          sort_asc_label: Asc
 | 
			
		||||
          sort_desc_label: Desc
 | 
			
		||||
      access:
 | 
			
		||||
        type: perm
 | 
			
		||||
        options:
 | 
			
		||||
          perm: 'access user profiles'
 | 
			
		||||
      cache:
 | 
			
		||||
        type: tag
 | 
			
		||||
        options: {  }
 | 
			
		||||
      empty: {  }
 | 
			
		||||
      sorts:
 | 
			
		||||
        vid:
 | 
			
		||||
          id: vid
 | 
			
		||||
          table: node_field_data
 | 
			
		||||
          field: vid
 | 
			
		||||
          relationship: uid
 | 
			
		||||
          group_type: group
 | 
			
		||||
          admin_label: ''
 | 
			
		||||
          entity_type: node
 | 
			
		||||
          entity_field: vid
 | 
			
		||||
          plugin_id: standard
 | 
			
		||||
          order: ASC
 | 
			
		||||
          expose:
 | 
			
		||||
            label: ''
 | 
			
		||||
          exposed: false
 | 
			
		||||
      arguments: {  }
 | 
			
		||||
      filters:
 | 
			
		||||
        moderation_state:
 | 
			
		||||
          id: moderation_state
 | 
			
		||||
          table: node_field_data
 | 
			
		||||
          field: moderation_state
 | 
			
		||||
          relationship: uid
 | 
			
		||||
          group_type: group
 | 
			
		||||
          admin_label: ''
 | 
			
		||||
          entity_type: node
 | 
			
		||||
          plugin_id: moderation_state_filter
 | 
			
		||||
          operator: in
 | 
			
		||||
          value: {  }
 | 
			
		||||
          group: 1
 | 
			
		||||
          exposed: true
 | 
			
		||||
          expose:
 | 
			
		||||
            operator_id: moderation_state_op
 | 
			
		||||
            label: 'Moderation state'
 | 
			
		||||
            description: ''
 | 
			
		||||
            use_operator: false
 | 
			
		||||
            operator: moderation_state_op
 | 
			
		||||
            identifier: moderation_state
 | 
			
		||||
            required: false
 | 
			
		||||
            remember: false
 | 
			
		||||
            multiple: false
 | 
			
		||||
            remember_roles:
 | 
			
		||||
              authenticated: authenticated
 | 
			
		||||
            reduce: false
 | 
			
		||||
          is_grouped: false
 | 
			
		||||
          group_info:
 | 
			
		||||
            label: ''
 | 
			
		||||
            description: ''
 | 
			
		||||
            identifier: ''
 | 
			
		||||
            optional: true
 | 
			
		||||
            widget: select
 | 
			
		||||
            multiple: false
 | 
			
		||||
            remember: false
 | 
			
		||||
            default_group: All
 | 
			
		||||
            default_group_multiple: {  }
 | 
			
		||||
            group_items: {  }
 | 
			
		||||
      style:
 | 
			
		||||
        type: default
 | 
			
		||||
      row:
 | 
			
		||||
        type: fields
 | 
			
		||||
      query:
 | 
			
		||||
        type: views_query
 | 
			
		||||
        options:
 | 
			
		||||
          query_comment: ''
 | 
			
		||||
          disable_sql_rewrite: false
 | 
			
		||||
          distinct: false
 | 
			
		||||
          replica: false
 | 
			
		||||
          query_tags: {  }
 | 
			
		||||
      relationships:
 | 
			
		||||
        uid:
 | 
			
		||||
          id: uid
 | 
			
		||||
          table: users_field_data
 | 
			
		||||
          field: uid
 | 
			
		||||
          relationship: none
 | 
			
		||||
          group_type: group
 | 
			
		||||
          admin_label: nodes
 | 
			
		||||
          entity_type: user
 | 
			
		||||
          entity_field: uid
 | 
			
		||||
          plugin_id: standard
 | 
			
		||||
          required: true
 | 
			
		||||
      header: {  }
 | 
			
		||||
      footer: {  }
 | 
			
		||||
      display_extenders: {  }
 | 
			
		||||
    cache_metadata:
 | 
			
		||||
      max-age: -1
 | 
			
		||||
      contexts:
 | 
			
		||||
        - 'languages:language_content'
 | 
			
		||||
        - 'languages:language_interface'
 | 
			
		||||
        - url
 | 
			
		||||
        - user.permissions
 | 
			
		||||
      tags:
 | 
			
		||||
        - 'config:workflow_list'
 | 
			
		||||
  page_1:
 | 
			
		||||
    id: page_1
 | 
			
		||||
    display_title: Page
 | 
			
		||||
    display_plugin: page
 | 
			
		||||
    position: 1
 | 
			
		||||
    display_options:
 | 
			
		||||
      display_extenders: {  }
 | 
			
		||||
      path: test-content-moderation-filter-relationship
 | 
			
		||||
    cache_metadata:
 | 
			
		||||
      max-age: -1
 | 
			
		||||
      contexts:
 | 
			
		||||
        - 'languages:language_content'
 | 
			
		||||
        - 'languages:language_interface'
 | 
			
		||||
        - url
 | 
			
		||||
        - user.permissions
 | 
			
		||||
      tags:
 | 
			
		||||
        - 'config:workflow_list'
 | 
			
		||||
@ -0,0 +1,349 @@
 | 
			
		||||
langcode: en
 | 
			
		||||
status: true
 | 
			
		||||
dependencies:
 | 
			
		||||
  module:
 | 
			
		||||
    - content_moderation
 | 
			
		||||
    - user
 | 
			
		||||
id: test_content_moderation_filter_via_revision_relationship
 | 
			
		||||
label: test_content_moderation_filter_via_revision_relationship
 | 
			
		||||
module: views
 | 
			
		||||
description: ''
 | 
			
		||||
tag: ''
 | 
			
		||||
base_table: users_field_data
 | 
			
		||||
base_field: uid
 | 
			
		||||
display:
 | 
			
		||||
  default:
 | 
			
		||||
    id: default
 | 
			
		||||
    display_title: Default
 | 
			
		||||
    display_plugin: default
 | 
			
		||||
    position: 0
 | 
			
		||||
    display_options:
 | 
			
		||||
      title: test_content_moderation_filter_via_revision_relationship
 | 
			
		||||
      fields:
 | 
			
		||||
        name:
 | 
			
		||||
          id: name
 | 
			
		||||
          table: users_field_data
 | 
			
		||||
          field: name
 | 
			
		||||
          relationship: none
 | 
			
		||||
          group_type: group
 | 
			
		||||
          admin_label: ''
 | 
			
		||||
          entity_type: user
 | 
			
		||||
          entity_field: name
 | 
			
		||||
          plugin_id: field
 | 
			
		||||
          label: ''
 | 
			
		||||
          exclude: false
 | 
			
		||||
          alter:
 | 
			
		||||
            alter_text: false
 | 
			
		||||
            text: ''
 | 
			
		||||
            make_link: false
 | 
			
		||||
            path: ''
 | 
			
		||||
            absolute: false
 | 
			
		||||
            external: false
 | 
			
		||||
            replace_spaces: false
 | 
			
		||||
            path_case: none
 | 
			
		||||
            trim_whitespace: false
 | 
			
		||||
            alt: ''
 | 
			
		||||
            rel: ''
 | 
			
		||||
            link_class: ''
 | 
			
		||||
            prefix: ''
 | 
			
		||||
            suffix: ''
 | 
			
		||||
            target: ''
 | 
			
		||||
            nl2br: false
 | 
			
		||||
            max_length: 0
 | 
			
		||||
            word_boundary: false
 | 
			
		||||
            ellipsis: false
 | 
			
		||||
            more_link: false
 | 
			
		||||
            more_link_text: ''
 | 
			
		||||
            more_link_path: ''
 | 
			
		||||
            strip_tags: false
 | 
			
		||||
            trim: false
 | 
			
		||||
            preserve_tags: ''
 | 
			
		||||
            html: false
 | 
			
		||||
          element_type: ''
 | 
			
		||||
          element_class: ''
 | 
			
		||||
          element_label_type: ''
 | 
			
		||||
          element_label_class: ''
 | 
			
		||||
          element_label_colon: false
 | 
			
		||||
          element_wrapper_type: ''
 | 
			
		||||
          element_wrapper_class: ''
 | 
			
		||||
          element_default_classes: true
 | 
			
		||||
          empty: ''
 | 
			
		||||
          hide_empty: false
 | 
			
		||||
          empty_zero: false
 | 
			
		||||
          hide_alter_empty: true
 | 
			
		||||
          click_sort_column: value
 | 
			
		||||
          type: user_name
 | 
			
		||||
          settings:
 | 
			
		||||
            link_to_entity: false
 | 
			
		||||
          group_column: value
 | 
			
		||||
          group_columns: {  }
 | 
			
		||||
          group_rows: true
 | 
			
		||||
          delta_limit: 0
 | 
			
		||||
          delta_offset: 0
 | 
			
		||||
          delta_reversed: false
 | 
			
		||||
          delta_first_last: false
 | 
			
		||||
          multi_type: separator
 | 
			
		||||
          separator: ', '
 | 
			
		||||
          field_api_classes: false
 | 
			
		||||
        title:
 | 
			
		||||
          id: title
 | 
			
		||||
          table: node_field_revision
 | 
			
		||||
          field: title
 | 
			
		||||
          relationship: uid_revision_test
 | 
			
		||||
          group_type: group
 | 
			
		||||
          admin_label: ''
 | 
			
		||||
          entity_type: node
 | 
			
		||||
          entity_field: title
 | 
			
		||||
          plugin_id: field
 | 
			
		||||
          label: ''
 | 
			
		||||
          exclude: false
 | 
			
		||||
          alter:
 | 
			
		||||
            alter_text: false
 | 
			
		||||
            text: ''
 | 
			
		||||
            make_link: false
 | 
			
		||||
            path: ''
 | 
			
		||||
            absolute: false
 | 
			
		||||
            external: false
 | 
			
		||||
            replace_spaces: false
 | 
			
		||||
            path_case: none
 | 
			
		||||
            trim_whitespace: false
 | 
			
		||||
            alt: ''
 | 
			
		||||
            rel: ''
 | 
			
		||||
            link_class: ''
 | 
			
		||||
            prefix: ''
 | 
			
		||||
            suffix: ''
 | 
			
		||||
            target: ''
 | 
			
		||||
            nl2br: false
 | 
			
		||||
            max_length: 0
 | 
			
		||||
            word_boundary: true
 | 
			
		||||
            ellipsis: true
 | 
			
		||||
            more_link: false
 | 
			
		||||
            more_link_text: ''
 | 
			
		||||
            more_link_path: ''
 | 
			
		||||
            strip_tags: false
 | 
			
		||||
            trim: false
 | 
			
		||||
            preserve_tags: ''
 | 
			
		||||
            html: false
 | 
			
		||||
          element_type: ''
 | 
			
		||||
          element_class: ''
 | 
			
		||||
          element_label_type: ''
 | 
			
		||||
          element_label_class: ''
 | 
			
		||||
          element_label_colon: false
 | 
			
		||||
          element_wrapper_type: ''
 | 
			
		||||
          element_wrapper_class: ''
 | 
			
		||||
          element_default_classes: true
 | 
			
		||||
          empty: ''
 | 
			
		||||
          hide_empty: false
 | 
			
		||||
          empty_zero: false
 | 
			
		||||
          hide_alter_empty: true
 | 
			
		||||
          click_sort_column: value
 | 
			
		||||
          type: string
 | 
			
		||||
          settings:
 | 
			
		||||
            link_to_entity: false
 | 
			
		||||
          group_column: value
 | 
			
		||||
          group_columns: {  }
 | 
			
		||||
          group_rows: true
 | 
			
		||||
          delta_limit: 0
 | 
			
		||||
          delta_offset: 0
 | 
			
		||||
          delta_reversed: false
 | 
			
		||||
          delta_first_last: false
 | 
			
		||||
          multi_type: separator
 | 
			
		||||
          separator: ', '
 | 
			
		||||
          field_api_classes: false
 | 
			
		||||
        moderation_state:
 | 
			
		||||
          id: moderation_state
 | 
			
		||||
          table: node_field_revision
 | 
			
		||||
          field: moderation_state
 | 
			
		||||
          relationship: uid_revision_test
 | 
			
		||||
          group_type: group
 | 
			
		||||
          admin_label: ''
 | 
			
		||||
          entity_type: node
 | 
			
		||||
          plugin_id: moderation_state_field
 | 
			
		||||
          label: ''
 | 
			
		||||
          exclude: false
 | 
			
		||||
          alter:
 | 
			
		||||
            alter_text: false
 | 
			
		||||
            text: ''
 | 
			
		||||
            make_link: false
 | 
			
		||||
            path: ''
 | 
			
		||||
            absolute: false
 | 
			
		||||
            external: false
 | 
			
		||||
            replace_spaces: false
 | 
			
		||||
            path_case: none
 | 
			
		||||
            trim_whitespace: false
 | 
			
		||||
            alt: ''
 | 
			
		||||
            rel: ''
 | 
			
		||||
            link_class: ''
 | 
			
		||||
            prefix: ''
 | 
			
		||||
            suffix: ''
 | 
			
		||||
            target: ''
 | 
			
		||||
            nl2br: false
 | 
			
		||||
            max_length: 0
 | 
			
		||||
            word_boundary: true
 | 
			
		||||
            ellipsis: true
 | 
			
		||||
            more_link: false
 | 
			
		||||
            more_link_text: ''
 | 
			
		||||
            more_link_path: ''
 | 
			
		||||
            strip_tags: false
 | 
			
		||||
            trim: false
 | 
			
		||||
            preserve_tags: ''
 | 
			
		||||
            html: false
 | 
			
		||||
          element_type: ''
 | 
			
		||||
          element_class: ''
 | 
			
		||||
          element_label_type: ''
 | 
			
		||||
          element_label_class: ''
 | 
			
		||||
          element_label_colon: false
 | 
			
		||||
          element_wrapper_type: ''
 | 
			
		||||
          element_wrapper_class: ''
 | 
			
		||||
          element_default_classes: true
 | 
			
		||||
          empty: ''
 | 
			
		||||
          hide_empty: false
 | 
			
		||||
          empty_zero: false
 | 
			
		||||
          hide_alter_empty: true
 | 
			
		||||
          click_sort_column: value
 | 
			
		||||
          type: content_moderation_state
 | 
			
		||||
          settings: {  }
 | 
			
		||||
          group_column: value
 | 
			
		||||
          group_columns: {  }
 | 
			
		||||
          group_rows: true
 | 
			
		||||
          delta_limit: 0
 | 
			
		||||
          delta_offset: 0
 | 
			
		||||
          delta_reversed: false
 | 
			
		||||
          delta_first_last: false
 | 
			
		||||
          multi_type: separator
 | 
			
		||||
          separator: ', '
 | 
			
		||||
          field_api_classes: false
 | 
			
		||||
      pager:
 | 
			
		||||
        type: none
 | 
			
		||||
        options:
 | 
			
		||||
          offset: 0
 | 
			
		||||
      exposed_form:
 | 
			
		||||
        type: basic
 | 
			
		||||
        options:
 | 
			
		||||
          submit_button: Apply
 | 
			
		||||
          reset_button: false
 | 
			
		||||
          reset_button_label: Reset
 | 
			
		||||
          exposed_sorts_label: 'Sort by'
 | 
			
		||||
          expose_sort_order: true
 | 
			
		||||
          sort_asc_label: Asc
 | 
			
		||||
          sort_desc_label: Desc
 | 
			
		||||
      access:
 | 
			
		||||
        type: perm
 | 
			
		||||
        options:
 | 
			
		||||
          perm: 'access user profiles'
 | 
			
		||||
      cache:
 | 
			
		||||
        type: tag
 | 
			
		||||
        options: {  }
 | 
			
		||||
      empty: {  }
 | 
			
		||||
      sorts:
 | 
			
		||||
        vid:
 | 
			
		||||
          id: vid
 | 
			
		||||
          table: node_field_revision
 | 
			
		||||
          field: vid
 | 
			
		||||
          relationship: uid_revision_test
 | 
			
		||||
          group_type: group
 | 
			
		||||
          admin_label: ''
 | 
			
		||||
          entity_type: node
 | 
			
		||||
          entity_field: vid
 | 
			
		||||
          plugin_id: standard
 | 
			
		||||
          order: ASC
 | 
			
		||||
          expose:
 | 
			
		||||
            label: ''
 | 
			
		||||
            field_identifier: ''
 | 
			
		||||
          exposed: false
 | 
			
		||||
      arguments: {  }
 | 
			
		||||
      filters:
 | 
			
		||||
        moderation_state:
 | 
			
		||||
          id: moderation_state
 | 
			
		||||
          table: node_field_revision
 | 
			
		||||
          field: moderation_state
 | 
			
		||||
          relationship: uid_revision_test
 | 
			
		||||
          group_type: group
 | 
			
		||||
          admin_label: ''
 | 
			
		||||
          entity_type: node
 | 
			
		||||
          plugin_id: moderation_state_filter
 | 
			
		||||
          operator: in
 | 
			
		||||
          value: {  }
 | 
			
		||||
          group: 1
 | 
			
		||||
          exposed: true
 | 
			
		||||
          expose:
 | 
			
		||||
            operator_id: moderation_state_op
 | 
			
		||||
            label: 'Moderation state'
 | 
			
		||||
            description: ''
 | 
			
		||||
            use_operator: false
 | 
			
		||||
            operator: moderation_state_op
 | 
			
		||||
            operator_limit_selection: false
 | 
			
		||||
            operator_list: {  }
 | 
			
		||||
            identifier: moderation_state
 | 
			
		||||
            required: false
 | 
			
		||||
            remember: false
 | 
			
		||||
            multiple: false
 | 
			
		||||
            remember_roles:
 | 
			
		||||
              authenticated: authenticated
 | 
			
		||||
            reduce: false
 | 
			
		||||
          is_grouped: false
 | 
			
		||||
          group_info:
 | 
			
		||||
            label: ''
 | 
			
		||||
            description: ''
 | 
			
		||||
            identifier: ''
 | 
			
		||||
            optional: true
 | 
			
		||||
            widget: select
 | 
			
		||||
            multiple: false
 | 
			
		||||
            remember: false
 | 
			
		||||
            default_group: All
 | 
			
		||||
            default_group_multiple: {  }
 | 
			
		||||
            group_items: {  }
 | 
			
		||||
      style:
 | 
			
		||||
        type: default
 | 
			
		||||
      row:
 | 
			
		||||
        type: fields
 | 
			
		||||
      query:
 | 
			
		||||
        type: views_query
 | 
			
		||||
        options:
 | 
			
		||||
          query_comment: ''
 | 
			
		||||
          disable_sql_rewrite: false
 | 
			
		||||
          distinct: false
 | 
			
		||||
          replica: false
 | 
			
		||||
          query_tags: {  }
 | 
			
		||||
      relationships:
 | 
			
		||||
        uid_revision_test:
 | 
			
		||||
          id: uid_revision_test
 | 
			
		||||
          table: users_field_data
 | 
			
		||||
          field: uid_revision_test
 | 
			
		||||
          relationship: none
 | 
			
		||||
          group_type: group
 | 
			
		||||
          admin_label: 'node revisions'
 | 
			
		||||
          entity_type: user
 | 
			
		||||
          plugin_id: standard
 | 
			
		||||
          required: true
 | 
			
		||||
      header: {  }
 | 
			
		||||
      footer: {  }
 | 
			
		||||
      display_extenders: {  }
 | 
			
		||||
    cache_metadata:
 | 
			
		||||
      max-age: -1
 | 
			
		||||
      contexts:
 | 
			
		||||
        - 'languages:language_content'
 | 
			
		||||
        - 'languages:language_interface'
 | 
			
		||||
        - url
 | 
			
		||||
        - url.query_args
 | 
			
		||||
        - user.permissions
 | 
			
		||||
      tags:
 | 
			
		||||
        - 'config:workflow_list'
 | 
			
		||||
  page_1:
 | 
			
		||||
    id: page_1
 | 
			
		||||
    display_title: Page
 | 
			
		||||
    display_plugin: page
 | 
			
		||||
    position: 1
 | 
			
		||||
    display_options:
 | 
			
		||||
      display_extenders: {  }
 | 
			
		||||
      path: test-content-moderation-filter-revision-relationship
 | 
			
		||||
    cache_metadata:
 | 
			
		||||
      max-age: -1
 | 
			
		||||
      contexts:
 | 
			
		||||
        - 'languages:language_content'
 | 
			
		||||
        - 'languages:language_interface'
 | 
			
		||||
        - url
 | 
			
		||||
        - url.query_args
 | 
			
		||||
        - user.permissions
 | 
			
		||||
      tags:
 | 
			
		||||
        - 'config:workflow_list'
 | 
			
		||||
@ -0,0 +1,257 @@
 | 
			
		||||
langcode: en
 | 
			
		||||
status: true
 | 
			
		||||
dependencies:
 | 
			
		||||
  module:
 | 
			
		||||
    - content_moderation
 | 
			
		||||
    - node
 | 
			
		||||
    - user
 | 
			
		||||
id: test_content_moderation_state_filter_base_table
 | 
			
		||||
label: test_content_moderation_state_filter_base_table
 | 
			
		||||
module: views
 | 
			
		||||
description: ''
 | 
			
		||||
tag: ''
 | 
			
		||||
base_table: node_field_data
 | 
			
		||||
base_field: nid
 | 
			
		||||
display:
 | 
			
		||||
  default:
 | 
			
		||||
    id: default
 | 
			
		||||
    display_title: Default
 | 
			
		||||
    display_plugin: default
 | 
			
		||||
    position: 0
 | 
			
		||||
    display_options:
 | 
			
		||||
      fields:
 | 
			
		||||
        nid:
 | 
			
		||||
          id: nid
 | 
			
		||||
          table: node_field_data
 | 
			
		||||
          field: nid
 | 
			
		||||
          relationship: none
 | 
			
		||||
          group_type: group
 | 
			
		||||
          admin_label: ''
 | 
			
		||||
          entity_type: node
 | 
			
		||||
          entity_field: nid
 | 
			
		||||
          plugin_id: field
 | 
			
		||||
          label: ''
 | 
			
		||||
          exclude: false
 | 
			
		||||
          alter:
 | 
			
		||||
            alter_text: false
 | 
			
		||||
            text: ''
 | 
			
		||||
            make_link: false
 | 
			
		||||
            path: ''
 | 
			
		||||
            absolute: false
 | 
			
		||||
            external: false
 | 
			
		||||
            replace_spaces: false
 | 
			
		||||
            path_case: none
 | 
			
		||||
            trim_whitespace: false
 | 
			
		||||
            alt: ''
 | 
			
		||||
            rel: ''
 | 
			
		||||
            link_class: ''
 | 
			
		||||
            prefix: ''
 | 
			
		||||
            suffix: ''
 | 
			
		||||
            target: ''
 | 
			
		||||
            nl2br: false
 | 
			
		||||
            max_length: 0
 | 
			
		||||
            word_boundary: true
 | 
			
		||||
            ellipsis: true
 | 
			
		||||
            more_link: false
 | 
			
		||||
            more_link_text: ''
 | 
			
		||||
            more_link_path: ''
 | 
			
		||||
            strip_tags: false
 | 
			
		||||
            trim: false
 | 
			
		||||
            preserve_tags: ''
 | 
			
		||||
            html: false
 | 
			
		||||
          element_type: ''
 | 
			
		||||
          element_class: ''
 | 
			
		||||
          element_label_type: ''
 | 
			
		||||
          element_label_class: ''
 | 
			
		||||
          element_label_colon: false
 | 
			
		||||
          element_wrapper_type: ''
 | 
			
		||||
          element_wrapper_class: ''
 | 
			
		||||
          element_default_classes: true
 | 
			
		||||
          empty: ''
 | 
			
		||||
          hide_empty: false
 | 
			
		||||
          empty_zero: false
 | 
			
		||||
          hide_alter_empty: true
 | 
			
		||||
          click_sort_column: value
 | 
			
		||||
          type: number_integer
 | 
			
		||||
          settings:
 | 
			
		||||
            thousand_separator: ''
 | 
			
		||||
            prefix_suffix: false
 | 
			
		||||
          group_column: value
 | 
			
		||||
          group_columns: {  }
 | 
			
		||||
          group_rows: true
 | 
			
		||||
          delta_limit: 0
 | 
			
		||||
          delta_offset: 0
 | 
			
		||||
          delta_reversed: false
 | 
			
		||||
          delta_first_last: false
 | 
			
		||||
          multi_type: separator
 | 
			
		||||
          separator: ', '
 | 
			
		||||
          field_api_classes: false
 | 
			
		||||
      pager:
 | 
			
		||||
        type: none
 | 
			
		||||
        options:
 | 
			
		||||
          offset: 0
 | 
			
		||||
      exposed_form:
 | 
			
		||||
        type: basic
 | 
			
		||||
        options:
 | 
			
		||||
          submit_button: Apply
 | 
			
		||||
          reset_button: false
 | 
			
		||||
          reset_button_label: Reset
 | 
			
		||||
          exposed_sorts_label: 'Sort by'
 | 
			
		||||
          expose_sort_order: true
 | 
			
		||||
          sort_asc_label: Asc
 | 
			
		||||
          sort_desc_label: Desc
 | 
			
		||||
      access:
 | 
			
		||||
        type: perm
 | 
			
		||||
        options:
 | 
			
		||||
          perm: 'access content'
 | 
			
		||||
      cache:
 | 
			
		||||
        type: tag
 | 
			
		||||
        options: {  }
 | 
			
		||||
      empty: {  }
 | 
			
		||||
      sorts:
 | 
			
		||||
        nid:
 | 
			
		||||
          id: nid
 | 
			
		||||
          table: node_field_data
 | 
			
		||||
          field: nid
 | 
			
		||||
          relationship: none
 | 
			
		||||
          group_type: group
 | 
			
		||||
          admin_label: ''
 | 
			
		||||
          entity_type: node
 | 
			
		||||
          entity_field: nid
 | 
			
		||||
          plugin_id: standard
 | 
			
		||||
          order: ASC
 | 
			
		||||
          expose:
 | 
			
		||||
            label: ''
 | 
			
		||||
          exposed: false
 | 
			
		||||
      arguments: {  }
 | 
			
		||||
      filters:
 | 
			
		||||
        moderation_state:
 | 
			
		||||
          id: moderation_state
 | 
			
		||||
          table: node_field_data
 | 
			
		||||
          field: moderation_state
 | 
			
		||||
          relationship: none
 | 
			
		||||
          group_type: group
 | 
			
		||||
          admin_label: ''
 | 
			
		||||
          entity_type: node
 | 
			
		||||
          plugin_id: moderation_state_filter
 | 
			
		||||
          operator: in
 | 
			
		||||
          value: {  }
 | 
			
		||||
          group: 1
 | 
			
		||||
          exposed: true
 | 
			
		||||
          expose:
 | 
			
		||||
            operator_id: moderation_state_op
 | 
			
		||||
            label: 'Default Revision State'
 | 
			
		||||
            description: ''
 | 
			
		||||
            use_operator: false
 | 
			
		||||
            operator: moderation_state_op
 | 
			
		||||
            identifier: default_revision_state
 | 
			
		||||
            required: false
 | 
			
		||||
            remember: false
 | 
			
		||||
            multiple: false
 | 
			
		||||
            remember_roles:
 | 
			
		||||
              authenticated: authenticated
 | 
			
		||||
            reduce: false
 | 
			
		||||
          is_grouped: false
 | 
			
		||||
          group_info:
 | 
			
		||||
            label: ''
 | 
			
		||||
            description: ''
 | 
			
		||||
            identifier: ''
 | 
			
		||||
            optional: true
 | 
			
		||||
            widget: select
 | 
			
		||||
            multiple: false
 | 
			
		||||
            remember: false
 | 
			
		||||
            default_group: All
 | 
			
		||||
            default_group_multiple: {  }
 | 
			
		||||
            group_items: {  }
 | 
			
		||||
        moderation_state_1:
 | 
			
		||||
          id: moderation_state_1
 | 
			
		||||
          table: node_field_data
 | 
			
		||||
          field: moderation_state
 | 
			
		||||
          relationship: none
 | 
			
		||||
          group_type: group
 | 
			
		||||
          admin_label: ''
 | 
			
		||||
          entity_type: node
 | 
			
		||||
          plugin_id: moderation_state_filter
 | 
			
		||||
          operator: 'not empty'
 | 
			
		||||
          value: {  }
 | 
			
		||||
          group: 1
 | 
			
		||||
          exposed: false
 | 
			
		||||
          expose:
 | 
			
		||||
            operator_id: ''
 | 
			
		||||
            label: ''
 | 
			
		||||
            description: ''
 | 
			
		||||
            use_operator: false
 | 
			
		||||
            operator: ''
 | 
			
		||||
            identifier: ''
 | 
			
		||||
            required: false
 | 
			
		||||
            remember: false
 | 
			
		||||
            multiple: false
 | 
			
		||||
            remember_roles:
 | 
			
		||||
              authenticated: authenticated
 | 
			
		||||
            reduce: false
 | 
			
		||||
          is_grouped: false
 | 
			
		||||
          group_info:
 | 
			
		||||
            label: ''
 | 
			
		||||
            description: ''
 | 
			
		||||
            identifier: ''
 | 
			
		||||
            optional: true
 | 
			
		||||
            widget: select
 | 
			
		||||
            multiple: false
 | 
			
		||||
            remember: false
 | 
			
		||||
            default_group: All
 | 
			
		||||
            default_group_multiple: {  }
 | 
			
		||||
            group_items: {  }
 | 
			
		||||
      style:
 | 
			
		||||
        type: default
 | 
			
		||||
        options:
 | 
			
		||||
          grouping: {  }
 | 
			
		||||
          row_class: ''
 | 
			
		||||
          default_row_class: true
 | 
			
		||||
          uses_fields: false
 | 
			
		||||
      row:
 | 
			
		||||
        type: fields
 | 
			
		||||
        options:
 | 
			
		||||
          default_field_elements: true
 | 
			
		||||
          inline: {  }
 | 
			
		||||
          separator: ''
 | 
			
		||||
          hide_empty: false
 | 
			
		||||
      query:
 | 
			
		||||
        type: views_query
 | 
			
		||||
        options:
 | 
			
		||||
          query_comment: ''
 | 
			
		||||
          disable_sql_rewrite: false
 | 
			
		||||
          distinct: false
 | 
			
		||||
          replica: false
 | 
			
		||||
          query_tags: {  }
 | 
			
		||||
      relationships: {  }
 | 
			
		||||
      header: {  }
 | 
			
		||||
      footer: {  }
 | 
			
		||||
      display_extenders: {  }
 | 
			
		||||
    cache_metadata:
 | 
			
		||||
      max-age: -1
 | 
			
		||||
      contexts:
 | 
			
		||||
        - 'languages:language_content'
 | 
			
		||||
        - 'languages:language_interface'
 | 
			
		||||
        - url
 | 
			
		||||
        - 'user.node_grants:view'
 | 
			
		||||
        - user.permissions
 | 
			
		||||
      tags:
 | 
			
		||||
        - 'config:workflow_list'
 | 
			
		||||
  page_1:
 | 
			
		||||
    id: page_1
 | 
			
		||||
    display_title: Page
 | 
			
		||||
    display_plugin: page
 | 
			
		||||
    position: 1
 | 
			
		||||
    display_options:
 | 
			
		||||
      display_extenders: {  }
 | 
			
		||||
      path: filter-test-path
 | 
			
		||||
    cache_metadata:
 | 
			
		||||
      max-age: -1
 | 
			
		||||
      contexts:
 | 
			
		||||
        - 'languages:language_content'
 | 
			
		||||
        - 'languages:language_interface'
 | 
			
		||||
        - url
 | 
			
		||||
        - 'user.node_grants:view'
 | 
			
		||||
        - user.permissions
 | 
			
		||||
      tags:
 | 
			
		||||
        - 'config:workflow_list'
 | 
			
		||||
@ -0,0 +1,266 @@
 | 
			
		||||
langcode: en
 | 
			
		||||
status: true
 | 
			
		||||
dependencies:
 | 
			
		||||
  module:
 | 
			
		||||
    - content_moderation
 | 
			
		||||
    - node
 | 
			
		||||
    - user
 | 
			
		||||
id: test_content_moderation_state_filter_base_table_filter_group_or
 | 
			
		||||
label: test_content_moderation_state_filter_base_table_filter_group_or
 | 
			
		||||
module: views
 | 
			
		||||
description: ''
 | 
			
		||||
tag: ''
 | 
			
		||||
base_table: node_field_data
 | 
			
		||||
base_field: nid
 | 
			
		||||
display:
 | 
			
		||||
  default:
 | 
			
		||||
    id: default
 | 
			
		||||
    display_title: Default
 | 
			
		||||
    display_plugin: default
 | 
			
		||||
    position: 0
 | 
			
		||||
    display_options:
 | 
			
		||||
      fields:
 | 
			
		||||
        nid:
 | 
			
		||||
          id: nid
 | 
			
		||||
          table: node_field_data
 | 
			
		||||
          field: nid
 | 
			
		||||
          relationship: none
 | 
			
		||||
          group_type: group
 | 
			
		||||
          admin_label: ''
 | 
			
		||||
          entity_type: node
 | 
			
		||||
          entity_field: nid
 | 
			
		||||
          plugin_id: field
 | 
			
		||||
          label: ''
 | 
			
		||||
          exclude: false
 | 
			
		||||
          alter:
 | 
			
		||||
            alter_text: false
 | 
			
		||||
            text: ''
 | 
			
		||||
            make_link: false
 | 
			
		||||
            path: ''
 | 
			
		||||
            absolute: false
 | 
			
		||||
            external: false
 | 
			
		||||
            replace_spaces: false
 | 
			
		||||
            path_case: none
 | 
			
		||||
            trim_whitespace: false
 | 
			
		||||
            alt: ''
 | 
			
		||||
            rel: ''
 | 
			
		||||
            link_class: ''
 | 
			
		||||
            prefix: ''
 | 
			
		||||
            suffix: ''
 | 
			
		||||
            target: ''
 | 
			
		||||
            nl2br: false
 | 
			
		||||
            max_length: 0
 | 
			
		||||
            word_boundary: true
 | 
			
		||||
            ellipsis: true
 | 
			
		||||
            more_link: false
 | 
			
		||||
            more_link_text: ''
 | 
			
		||||
            more_link_path: ''
 | 
			
		||||
            strip_tags: false
 | 
			
		||||
            trim: false
 | 
			
		||||
            preserve_tags: ''
 | 
			
		||||
            html: false
 | 
			
		||||
          element_type: ''
 | 
			
		||||
          element_class: ''
 | 
			
		||||
          element_label_type: ''
 | 
			
		||||
          element_label_class: ''
 | 
			
		||||
          element_label_colon: false
 | 
			
		||||
          element_wrapper_type: ''
 | 
			
		||||
          element_wrapper_class: ''
 | 
			
		||||
          element_default_classes: true
 | 
			
		||||
          empty: ''
 | 
			
		||||
          hide_empty: false
 | 
			
		||||
          empty_zero: false
 | 
			
		||||
          hide_alter_empty: true
 | 
			
		||||
          click_sort_column: value
 | 
			
		||||
          type: number_integer
 | 
			
		||||
          settings:
 | 
			
		||||
            thousand_separator: ''
 | 
			
		||||
            prefix_suffix: false
 | 
			
		||||
          group_column: value
 | 
			
		||||
          group_columns: {  }
 | 
			
		||||
          group_rows: true
 | 
			
		||||
          delta_limit: 0
 | 
			
		||||
          delta_offset: 0
 | 
			
		||||
          delta_reversed: false
 | 
			
		||||
          delta_first_last: false
 | 
			
		||||
          multi_type: separator
 | 
			
		||||
          separator: ', '
 | 
			
		||||
          field_api_classes: false
 | 
			
		||||
      pager:
 | 
			
		||||
        type: none
 | 
			
		||||
        options:
 | 
			
		||||
          offset: 0
 | 
			
		||||
      exposed_form:
 | 
			
		||||
        type: basic
 | 
			
		||||
        options:
 | 
			
		||||
          submit_button: Apply
 | 
			
		||||
          reset_button: false
 | 
			
		||||
          reset_button_label: Reset
 | 
			
		||||
          exposed_sorts_label: 'Sort by'
 | 
			
		||||
          expose_sort_order: true
 | 
			
		||||
          sort_asc_label: Asc
 | 
			
		||||
          sort_desc_label: Desc
 | 
			
		||||
      access:
 | 
			
		||||
        type: perm
 | 
			
		||||
        options:
 | 
			
		||||
          perm: 'access content'
 | 
			
		||||
      cache:
 | 
			
		||||
        type: tag
 | 
			
		||||
        options: {  }
 | 
			
		||||
      empty: {  }
 | 
			
		||||
      sorts:
 | 
			
		||||
        nid:
 | 
			
		||||
          id: nid
 | 
			
		||||
          table: node_field_data
 | 
			
		||||
          field: nid
 | 
			
		||||
          relationship: none
 | 
			
		||||
          group_type: group
 | 
			
		||||
          admin_label: ''
 | 
			
		||||
          entity_type: node
 | 
			
		||||
          entity_field: nid
 | 
			
		||||
          plugin_id: standard
 | 
			
		||||
          order: ASC
 | 
			
		||||
          expose:
 | 
			
		||||
            label: ''
 | 
			
		||||
          exposed: false
 | 
			
		||||
      arguments: {  }
 | 
			
		||||
      filters:
 | 
			
		||||
        moderation_state:
 | 
			
		||||
          id: moderation_state
 | 
			
		||||
          table: node_field_data
 | 
			
		||||
          field: moderation_state
 | 
			
		||||
          relationship: none
 | 
			
		||||
          group_type: group
 | 
			
		||||
          admin_label: ''
 | 
			
		||||
          entity_type: node
 | 
			
		||||
          plugin_id: moderation_state_filter
 | 
			
		||||
          operator: in
 | 
			
		||||
          value: {  }
 | 
			
		||||
          group: 1
 | 
			
		||||
          exposed: true
 | 
			
		||||
          expose:
 | 
			
		||||
            operator_id: moderation_state_op
 | 
			
		||||
            label: 'Default Revision State'
 | 
			
		||||
            description: ''
 | 
			
		||||
            use_operator: false
 | 
			
		||||
            operator: moderation_state_op
 | 
			
		||||
            operator_limit_selection: false
 | 
			
		||||
            operator_list: {  }
 | 
			
		||||
            identifier: default_revision_state
 | 
			
		||||
            required: false
 | 
			
		||||
            remember: false
 | 
			
		||||
            multiple: false
 | 
			
		||||
            remember_roles:
 | 
			
		||||
              authenticated: authenticated
 | 
			
		||||
            reduce: false
 | 
			
		||||
          is_grouped: false
 | 
			
		||||
          group_info:
 | 
			
		||||
            label: ''
 | 
			
		||||
            description: ''
 | 
			
		||||
            identifier: ''
 | 
			
		||||
            optional: true
 | 
			
		||||
            widget: select
 | 
			
		||||
            multiple: false
 | 
			
		||||
            remember: false
 | 
			
		||||
            default_group: All
 | 
			
		||||
            default_group_multiple: {  }
 | 
			
		||||
            group_items: {  }
 | 
			
		||||
        moderation_state_1:
 | 
			
		||||
          id: moderation_state_1
 | 
			
		||||
          table: node_field_data
 | 
			
		||||
          field: moderation_state
 | 
			
		||||
          relationship: none
 | 
			
		||||
          group_type: group
 | 
			
		||||
          admin_label: ''
 | 
			
		||||
          entity_type: node
 | 
			
		||||
          plugin_id: moderation_state_filter
 | 
			
		||||
          operator: 'not empty'
 | 
			
		||||
          value: {  }
 | 
			
		||||
          group: 2
 | 
			
		||||
          exposed: false
 | 
			
		||||
          expose:
 | 
			
		||||
            operator_id: ''
 | 
			
		||||
            label: ''
 | 
			
		||||
            description: ''
 | 
			
		||||
            use_operator: false
 | 
			
		||||
            operator: ''
 | 
			
		||||
            operator_limit_selection: false
 | 
			
		||||
            operator_list: {  }
 | 
			
		||||
            identifier: ''
 | 
			
		||||
            required: false
 | 
			
		||||
            remember: false
 | 
			
		||||
            multiple: false
 | 
			
		||||
            remember_roles:
 | 
			
		||||
              authenticated: authenticated
 | 
			
		||||
            reduce: false
 | 
			
		||||
          is_grouped: false
 | 
			
		||||
          group_info:
 | 
			
		||||
            label: ''
 | 
			
		||||
            description: ''
 | 
			
		||||
            identifier: ''
 | 
			
		||||
            optional: true
 | 
			
		||||
            widget: select
 | 
			
		||||
            multiple: false
 | 
			
		||||
            remember: false
 | 
			
		||||
            default_group: All
 | 
			
		||||
            default_group_multiple: {  }
 | 
			
		||||
            group_items: {  }
 | 
			
		||||
      filter_groups:
 | 
			
		||||
        operator: AND
 | 
			
		||||
        groups:
 | 
			
		||||
          1: OR
 | 
			
		||||
          2: OR
 | 
			
		||||
      style:
 | 
			
		||||
        type: default
 | 
			
		||||
        options:
 | 
			
		||||
          grouping: {  }
 | 
			
		||||
          row_class: ''
 | 
			
		||||
          default_row_class: true
 | 
			
		||||
          uses_fields: false
 | 
			
		||||
      row:
 | 
			
		||||
        type: fields
 | 
			
		||||
        options:
 | 
			
		||||
          default_field_elements: true
 | 
			
		||||
          inline: {  }
 | 
			
		||||
          separator: ''
 | 
			
		||||
          hide_empty: false
 | 
			
		||||
      query:
 | 
			
		||||
        type: views_query
 | 
			
		||||
        options:
 | 
			
		||||
          query_comment: ''
 | 
			
		||||
          disable_sql_rewrite: false
 | 
			
		||||
          distinct: false
 | 
			
		||||
          replica: false
 | 
			
		||||
          query_tags: {  }
 | 
			
		||||
      relationships: {  }
 | 
			
		||||
      header: {  }
 | 
			
		||||
      footer: {  }
 | 
			
		||||
      display_extenders: {  }
 | 
			
		||||
    cache_metadata:
 | 
			
		||||
      max-age: -1
 | 
			
		||||
      contexts:
 | 
			
		||||
        - 'languages:language_content'
 | 
			
		||||
        - 'languages:language_interface'
 | 
			
		||||
        - url
 | 
			
		||||
        - 'user.node_grants:view'
 | 
			
		||||
        - user.permissions
 | 
			
		||||
      tags:
 | 
			
		||||
        - 'config:workflow_list'
 | 
			
		||||
  page_1:
 | 
			
		||||
    id: page_1
 | 
			
		||||
    display_title: Page
 | 
			
		||||
    display_plugin: page
 | 
			
		||||
    position: 1
 | 
			
		||||
    display_options:
 | 
			
		||||
      display_extenders: {  }
 | 
			
		||||
      path: filter-test-path
 | 
			
		||||
    cache_metadata:
 | 
			
		||||
      max-age: -1
 | 
			
		||||
      contexts:
 | 
			
		||||
        - 'languages:language_content'
 | 
			
		||||
        - 'languages:language_interface'
 | 
			
		||||
        - url
 | 
			
		||||
        - 'user.node_grants:view'
 | 
			
		||||
        - user.permissions
 | 
			
		||||
      tags:
 | 
			
		||||
        - 'config:workflow_list'
 | 
			
		||||
@ -0,0 +1,219 @@
 | 
			
		||||
langcode: en
 | 
			
		||||
status: true
 | 
			
		||||
dependencies:
 | 
			
		||||
  module:
 | 
			
		||||
    - content_moderation
 | 
			
		||||
    - node
 | 
			
		||||
    - user
 | 
			
		||||
id: test_content_moderation_state_filter_base_table_filter_on_revision
 | 
			
		||||
label: test_content_moderation_state_filter_base_table_filter_on_revision
 | 
			
		||||
module: views
 | 
			
		||||
description: ''
 | 
			
		||||
tag: ''
 | 
			
		||||
base_table: node_field_data
 | 
			
		||||
base_field: nid
 | 
			
		||||
display:
 | 
			
		||||
  default:
 | 
			
		||||
    id: default
 | 
			
		||||
    display_title: Default
 | 
			
		||||
    display_plugin: default
 | 
			
		||||
    position: 0
 | 
			
		||||
    display_options:
 | 
			
		||||
      fields:
 | 
			
		||||
        nid:
 | 
			
		||||
          id: nid
 | 
			
		||||
          table: node_field_data
 | 
			
		||||
          field: nid
 | 
			
		||||
          relationship: none
 | 
			
		||||
          group_type: group
 | 
			
		||||
          admin_label: ''
 | 
			
		||||
          entity_type: node
 | 
			
		||||
          entity_field: nid
 | 
			
		||||
          plugin_id: field
 | 
			
		||||
          label: ''
 | 
			
		||||
          exclude: false
 | 
			
		||||
          alter:
 | 
			
		||||
            alter_text: false
 | 
			
		||||
            text: ''
 | 
			
		||||
            make_link: false
 | 
			
		||||
            path: ''
 | 
			
		||||
            absolute: false
 | 
			
		||||
            external: false
 | 
			
		||||
            replace_spaces: false
 | 
			
		||||
            path_case: none
 | 
			
		||||
            trim_whitespace: false
 | 
			
		||||
            alt: ''
 | 
			
		||||
            rel: ''
 | 
			
		||||
            link_class: ''
 | 
			
		||||
            prefix: ''
 | 
			
		||||
            suffix: ''
 | 
			
		||||
            target: ''
 | 
			
		||||
            nl2br: false
 | 
			
		||||
            max_length: 0
 | 
			
		||||
            word_boundary: true
 | 
			
		||||
            ellipsis: true
 | 
			
		||||
            more_link: false
 | 
			
		||||
            more_link_text: ''
 | 
			
		||||
            more_link_path: ''
 | 
			
		||||
            strip_tags: false
 | 
			
		||||
            trim: false
 | 
			
		||||
            preserve_tags: ''
 | 
			
		||||
            html: false
 | 
			
		||||
          element_type: ''
 | 
			
		||||
          element_class: ''
 | 
			
		||||
          element_label_type: ''
 | 
			
		||||
          element_label_class: ''
 | 
			
		||||
          element_label_colon: false
 | 
			
		||||
          element_wrapper_type: ''
 | 
			
		||||
          element_wrapper_class: ''
 | 
			
		||||
          element_default_classes: true
 | 
			
		||||
          empty: ''
 | 
			
		||||
          hide_empty: false
 | 
			
		||||
          empty_zero: false
 | 
			
		||||
          hide_alter_empty: true
 | 
			
		||||
          click_sort_column: value
 | 
			
		||||
          type: number_integer
 | 
			
		||||
          settings:
 | 
			
		||||
            thousand_separator: ''
 | 
			
		||||
            prefix_suffix: false
 | 
			
		||||
          group_column: value
 | 
			
		||||
          group_columns: {  }
 | 
			
		||||
          group_rows: true
 | 
			
		||||
          delta_limit: 0
 | 
			
		||||
          delta_offset: 0
 | 
			
		||||
          delta_reversed: false
 | 
			
		||||
          delta_first_last: false
 | 
			
		||||
          multi_type: separator
 | 
			
		||||
          separator: ', '
 | 
			
		||||
          field_api_classes: false
 | 
			
		||||
      pager:
 | 
			
		||||
        type: none
 | 
			
		||||
        options:
 | 
			
		||||
          offset: 0
 | 
			
		||||
      exposed_form:
 | 
			
		||||
        type: basic
 | 
			
		||||
        options:
 | 
			
		||||
          submit_button: Apply
 | 
			
		||||
          reset_button: false
 | 
			
		||||
          reset_button_label: Reset
 | 
			
		||||
          exposed_sorts_label: 'Sort by'
 | 
			
		||||
          expose_sort_order: true
 | 
			
		||||
          sort_asc_label: Asc
 | 
			
		||||
          sort_desc_label: Desc
 | 
			
		||||
      access:
 | 
			
		||||
        type: perm
 | 
			
		||||
        options:
 | 
			
		||||
          perm: 'access content'
 | 
			
		||||
      cache:
 | 
			
		||||
        type: tag
 | 
			
		||||
        options: {  }
 | 
			
		||||
      empty: {  }
 | 
			
		||||
      sorts:
 | 
			
		||||
        nid:
 | 
			
		||||
          id: nid
 | 
			
		||||
          table: node_field_data
 | 
			
		||||
          field: nid
 | 
			
		||||
          relationship: none
 | 
			
		||||
          group_type: group
 | 
			
		||||
          admin_label: ''
 | 
			
		||||
          entity_type: node
 | 
			
		||||
          entity_field: nid
 | 
			
		||||
          plugin_id: standard
 | 
			
		||||
          order: ASC
 | 
			
		||||
          expose:
 | 
			
		||||
            label: ''
 | 
			
		||||
          exposed: false
 | 
			
		||||
      arguments: {  }
 | 
			
		||||
      filters:
 | 
			
		||||
        moderation_state:
 | 
			
		||||
          id: moderation_state
 | 
			
		||||
          table: node_field_revision
 | 
			
		||||
          field: moderation_state
 | 
			
		||||
          relationship: none
 | 
			
		||||
          group_type: group
 | 
			
		||||
          admin_label: ''
 | 
			
		||||
          entity_type: node
 | 
			
		||||
          plugin_id: moderation_state_filter
 | 
			
		||||
          operator: in
 | 
			
		||||
          value: {  }
 | 
			
		||||
          group: 1
 | 
			
		||||
          exposed: true
 | 
			
		||||
          expose:
 | 
			
		||||
            operator_id: moderation_state_op
 | 
			
		||||
            label: 'Moderation state'
 | 
			
		||||
            description: ''
 | 
			
		||||
            use_operator: false
 | 
			
		||||
            operator: moderation_state_op
 | 
			
		||||
            identifier: ''
 | 
			
		||||
            required: false
 | 
			
		||||
            remember: false
 | 
			
		||||
            multiple: false
 | 
			
		||||
            remember_roles:
 | 
			
		||||
              authenticated: authenticated
 | 
			
		||||
            reduce: false
 | 
			
		||||
          is_grouped: false
 | 
			
		||||
          group_info:
 | 
			
		||||
            label: ''
 | 
			
		||||
            description: ''
 | 
			
		||||
            identifier: ''
 | 
			
		||||
            optional: true
 | 
			
		||||
            widget: select
 | 
			
		||||
            multiple: false
 | 
			
		||||
            remember: false
 | 
			
		||||
            default_group: All
 | 
			
		||||
            default_group_multiple: {  }
 | 
			
		||||
            group_items: {  }
 | 
			
		||||
      style:
 | 
			
		||||
        type: default
 | 
			
		||||
        options:
 | 
			
		||||
          grouping: {  }
 | 
			
		||||
          row_class: ''
 | 
			
		||||
          default_row_class: true
 | 
			
		||||
          uses_fields: false
 | 
			
		||||
      row:
 | 
			
		||||
        type: fields
 | 
			
		||||
        options:
 | 
			
		||||
          default_field_elements: true
 | 
			
		||||
          inline: {  }
 | 
			
		||||
          separator: ''
 | 
			
		||||
          hide_empty: false
 | 
			
		||||
      query:
 | 
			
		||||
        type: views_query
 | 
			
		||||
        options:
 | 
			
		||||
          query_comment: ''
 | 
			
		||||
          disable_sql_rewrite: false
 | 
			
		||||
          distinct: false
 | 
			
		||||
          replica: false
 | 
			
		||||
          query_tags: {  }
 | 
			
		||||
      relationships: {  }
 | 
			
		||||
      header: {  }
 | 
			
		||||
      footer: {  }
 | 
			
		||||
      display_extenders: {  }
 | 
			
		||||
    cache_metadata:
 | 
			
		||||
      max-age: -1
 | 
			
		||||
      contexts:
 | 
			
		||||
        - 'languages:language_content'
 | 
			
		||||
        - 'languages:language_interface'
 | 
			
		||||
        - url
 | 
			
		||||
        - 'user.node_grants:view'
 | 
			
		||||
        - user.permissions
 | 
			
		||||
      tags:
 | 
			
		||||
        - 'config:workflow_list'
 | 
			
		||||
  page_1:
 | 
			
		||||
    id: page_1
 | 
			
		||||
    display_title: Page
 | 
			
		||||
    display_plugin: page
 | 
			
		||||
    position: 1
 | 
			
		||||
    display_options:
 | 
			
		||||
      display_extenders: {  }
 | 
			
		||||
      path: filter-on-revision-test-path
 | 
			
		||||
    cache_metadata:
 | 
			
		||||
      max-age: -1
 | 
			
		||||
      contexts:
 | 
			
		||||
        - 'languages:language_content'
 | 
			
		||||
        - 'languages:language_interface'
 | 
			
		||||
        - url
 | 
			
		||||
        - 'user.node_grants:view'
 | 
			
		||||
        - user.permissions
 | 
			
		||||
      tags:
 | 
			
		||||
        - 'config:workflow_list'
 | 
			
		||||
@ -0,0 +1,164 @@
 | 
			
		||||
langcode: en
 | 
			
		||||
status: true
 | 
			
		||||
dependencies:
 | 
			
		||||
  module:
 | 
			
		||||
    - content_moderation
 | 
			
		||||
    - entity_test
 | 
			
		||||
id: test_content_moderation_state_filter_entity_test
 | 
			
		||||
label: test_content_moderation_state_filter_entity_test
 | 
			
		||||
module: views
 | 
			
		||||
description: ''
 | 
			
		||||
tag: ''
 | 
			
		||||
base_table: entity_test_no_bundle
 | 
			
		||||
base_field: id
 | 
			
		||||
display:
 | 
			
		||||
  default:
 | 
			
		||||
    id: default
 | 
			
		||||
    display_title: Default
 | 
			
		||||
    display_plugin: default
 | 
			
		||||
    position: 0
 | 
			
		||||
    display_options:
 | 
			
		||||
      fields:
 | 
			
		||||
        entity_id:
 | 
			
		||||
          id: entity_id
 | 
			
		||||
          table: content_revision_tracker
 | 
			
		||||
          field: entity_id
 | 
			
		||||
          relationship: none
 | 
			
		||||
          group_type: group
 | 
			
		||||
          admin_label: ''
 | 
			
		||||
          plugin_id: standard
 | 
			
		||||
          label: ''
 | 
			
		||||
          exclude: false
 | 
			
		||||
          alter:
 | 
			
		||||
            alter_text: false
 | 
			
		||||
            text: ''
 | 
			
		||||
            make_link: false
 | 
			
		||||
            path: ''
 | 
			
		||||
            absolute: false
 | 
			
		||||
            external: false
 | 
			
		||||
            replace_spaces: false
 | 
			
		||||
            path_case: none
 | 
			
		||||
            trim_whitespace: false
 | 
			
		||||
            alt: ''
 | 
			
		||||
            rel: ''
 | 
			
		||||
            link_class: ''
 | 
			
		||||
            prefix: ''
 | 
			
		||||
            suffix: ''
 | 
			
		||||
            target: ''
 | 
			
		||||
            nl2br: false
 | 
			
		||||
            max_length: 0
 | 
			
		||||
            word_boundary: true
 | 
			
		||||
            ellipsis: true
 | 
			
		||||
            more_link: false
 | 
			
		||||
            more_link_text: ''
 | 
			
		||||
            more_link_path: ''
 | 
			
		||||
            strip_tags: false
 | 
			
		||||
            trim: false
 | 
			
		||||
            preserve_tags: ''
 | 
			
		||||
            html: false
 | 
			
		||||
          element_type: ''
 | 
			
		||||
          element_class: ''
 | 
			
		||||
          element_label_type: ''
 | 
			
		||||
          element_label_class: ''
 | 
			
		||||
          element_label_colon: false
 | 
			
		||||
          element_wrapper_type: ''
 | 
			
		||||
          element_wrapper_class: ''
 | 
			
		||||
          element_default_classes: true
 | 
			
		||||
          empty: ''
 | 
			
		||||
          hide_empty: false
 | 
			
		||||
          empty_zero: false
 | 
			
		||||
          hide_alter_empty: true
 | 
			
		||||
      pager:
 | 
			
		||||
        type: none
 | 
			
		||||
        options:
 | 
			
		||||
          offset: 0
 | 
			
		||||
      exposed_form:
 | 
			
		||||
        type: basic
 | 
			
		||||
        options:
 | 
			
		||||
          submit_button: Apply
 | 
			
		||||
          reset_button: false
 | 
			
		||||
          reset_button_label: Reset
 | 
			
		||||
          exposed_sorts_label: 'Sort by'
 | 
			
		||||
          expose_sort_order: true
 | 
			
		||||
          sort_asc_label: Asc
 | 
			
		||||
          sort_desc_label: Desc
 | 
			
		||||
      access:
 | 
			
		||||
        type: none
 | 
			
		||||
        options: {  }
 | 
			
		||||
      cache:
 | 
			
		||||
        type: tag
 | 
			
		||||
        options: {  }
 | 
			
		||||
      empty: {  }
 | 
			
		||||
      sorts: {  }
 | 
			
		||||
      arguments: {  }
 | 
			
		||||
      filters:
 | 
			
		||||
        moderation_state:
 | 
			
		||||
          id: moderation_state
 | 
			
		||||
          table: entity_test_no_bundle
 | 
			
		||||
          field: moderation_state
 | 
			
		||||
          relationship: none
 | 
			
		||||
          group_type: group
 | 
			
		||||
          admin_label: ''
 | 
			
		||||
          entity_type: entity_test_no_bundle
 | 
			
		||||
          plugin_id: moderation_state_filter
 | 
			
		||||
          operator: in
 | 
			
		||||
          value: {  }
 | 
			
		||||
          group: 1
 | 
			
		||||
          exposed: true
 | 
			
		||||
          expose:
 | 
			
		||||
            operator_id: moderation_state_op
 | 
			
		||||
            label: 'Moderation state'
 | 
			
		||||
            description: ''
 | 
			
		||||
            use_operator: false
 | 
			
		||||
            operator: moderation_state_op
 | 
			
		||||
            identifier: moderation_state
 | 
			
		||||
            required: false
 | 
			
		||||
            remember: false
 | 
			
		||||
            multiple: false
 | 
			
		||||
            remember_roles:
 | 
			
		||||
              authenticated: authenticated
 | 
			
		||||
            reduce: false
 | 
			
		||||
          is_grouped: false
 | 
			
		||||
          group_info:
 | 
			
		||||
            label: ''
 | 
			
		||||
            description: ''
 | 
			
		||||
            identifier: ''
 | 
			
		||||
            optional: true
 | 
			
		||||
            widget: select
 | 
			
		||||
            multiple: false
 | 
			
		||||
            remember: false
 | 
			
		||||
            default_group: All
 | 
			
		||||
            default_group_multiple: {  }
 | 
			
		||||
            group_items: {  }
 | 
			
		||||
      style:
 | 
			
		||||
        type: default
 | 
			
		||||
        options:
 | 
			
		||||
          grouping: {  }
 | 
			
		||||
          row_class: ''
 | 
			
		||||
          default_row_class: true
 | 
			
		||||
          uses_fields: false
 | 
			
		||||
      row:
 | 
			
		||||
        type: fields
 | 
			
		||||
        options:
 | 
			
		||||
          default_field_elements: true
 | 
			
		||||
          inline: {  }
 | 
			
		||||
          separator: ''
 | 
			
		||||
          hide_empty: false
 | 
			
		||||
      query:
 | 
			
		||||
        type: views_query
 | 
			
		||||
        options:
 | 
			
		||||
          query_comment: ''
 | 
			
		||||
          disable_sql_rewrite: false
 | 
			
		||||
          distinct: false
 | 
			
		||||
          replica: false
 | 
			
		||||
          query_tags: {  }
 | 
			
		||||
      relationships: {  }
 | 
			
		||||
      header: {  }
 | 
			
		||||
      footer: {  }
 | 
			
		||||
      display_extenders: {  }
 | 
			
		||||
    cache_metadata:
 | 
			
		||||
      max-age: -1
 | 
			
		||||
      contexts:
 | 
			
		||||
        - 'languages:language_interface'
 | 
			
		||||
        - url
 | 
			
		||||
      tags: {  }
 | 
			
		||||
@ -0,0 +1,214 @@
 | 
			
		||||
langcode: en
 | 
			
		||||
status: true
 | 
			
		||||
dependencies:
 | 
			
		||||
  module:
 | 
			
		||||
    - content_moderation
 | 
			
		||||
    - user
 | 
			
		||||
id: test_content_moderation_state_filter_revision_table
 | 
			
		||||
label: test_content_moderation_state_filter_revision_table
 | 
			
		||||
module: views
 | 
			
		||||
description: ''
 | 
			
		||||
tag: ''
 | 
			
		||||
base_table: node_field_revision
 | 
			
		||||
base_field: vid
 | 
			
		||||
display:
 | 
			
		||||
  default:
 | 
			
		||||
    id: default
 | 
			
		||||
    display_title: Default
 | 
			
		||||
    display_plugin: default
 | 
			
		||||
    position: 0
 | 
			
		||||
    display_options:
 | 
			
		||||
      fields:
 | 
			
		||||
        nid:
 | 
			
		||||
          id: nid
 | 
			
		||||
          table: node_field_revision
 | 
			
		||||
          field: nid
 | 
			
		||||
          relationship: none
 | 
			
		||||
          group_type: group
 | 
			
		||||
          admin_label: ''
 | 
			
		||||
          entity_type: node
 | 
			
		||||
          entity_field: nid
 | 
			
		||||
          plugin_id: field
 | 
			
		||||
          label: ''
 | 
			
		||||
          exclude: false
 | 
			
		||||
          alter:
 | 
			
		||||
            alter_text: false
 | 
			
		||||
            text: ''
 | 
			
		||||
            make_link: false
 | 
			
		||||
            path: ''
 | 
			
		||||
            absolute: false
 | 
			
		||||
            external: false
 | 
			
		||||
            replace_spaces: false
 | 
			
		||||
            path_case: none
 | 
			
		||||
            trim_whitespace: false
 | 
			
		||||
            alt: ''
 | 
			
		||||
            rel: ''
 | 
			
		||||
            link_class: ''
 | 
			
		||||
            prefix: ''
 | 
			
		||||
            suffix: ''
 | 
			
		||||
            target: ''
 | 
			
		||||
            nl2br: false
 | 
			
		||||
            max_length: 0
 | 
			
		||||
            word_boundary: true
 | 
			
		||||
            ellipsis: true
 | 
			
		||||
            more_link: false
 | 
			
		||||
            more_link_text: ''
 | 
			
		||||
            more_link_path: ''
 | 
			
		||||
            strip_tags: false
 | 
			
		||||
            trim: false
 | 
			
		||||
            preserve_tags: ''
 | 
			
		||||
            html: false
 | 
			
		||||
          element_type: ''
 | 
			
		||||
          element_class: ''
 | 
			
		||||
          element_label_type: ''
 | 
			
		||||
          element_label_class: ''
 | 
			
		||||
          element_label_colon: false
 | 
			
		||||
          element_wrapper_type: ''
 | 
			
		||||
          element_wrapper_class: ''
 | 
			
		||||
          element_default_classes: true
 | 
			
		||||
          empty: ''
 | 
			
		||||
          hide_empty: false
 | 
			
		||||
          empty_zero: false
 | 
			
		||||
          hide_alter_empty: true
 | 
			
		||||
          click_sort_column: value
 | 
			
		||||
          type: number_integer
 | 
			
		||||
          settings:
 | 
			
		||||
            thousand_separator: ''
 | 
			
		||||
            prefix_suffix: true
 | 
			
		||||
          group_column: value
 | 
			
		||||
          group_columns: {  }
 | 
			
		||||
          group_rows: true
 | 
			
		||||
          delta_limit: 0
 | 
			
		||||
          delta_offset: 0
 | 
			
		||||
          delta_reversed: false
 | 
			
		||||
          delta_first_last: false
 | 
			
		||||
          multi_type: separator
 | 
			
		||||
          separator: ', '
 | 
			
		||||
          field_api_classes: false
 | 
			
		||||
      pager:
 | 
			
		||||
        type: mini
 | 
			
		||||
        options:
 | 
			
		||||
          offset: 0
 | 
			
		||||
          items_per_page: 10
 | 
			
		||||
          total_pages: null
 | 
			
		||||
          id: 0
 | 
			
		||||
          tags:
 | 
			
		||||
            next: ››
 | 
			
		||||
            previous: ‹‹
 | 
			
		||||
          expose:
 | 
			
		||||
            items_per_page: false
 | 
			
		||||
            items_per_page_label: 'Items per page'
 | 
			
		||||
            items_per_page_options: '5, 10, 25, 50'
 | 
			
		||||
            items_per_page_options_all: false
 | 
			
		||||
            items_per_page_options_all_label: '- All -'
 | 
			
		||||
            offset: false
 | 
			
		||||
            offset_label: Offset
 | 
			
		||||
      exposed_form:
 | 
			
		||||
        type: basic
 | 
			
		||||
        options:
 | 
			
		||||
          submit_button: Apply
 | 
			
		||||
          reset_button: false
 | 
			
		||||
          reset_button_label: Reset
 | 
			
		||||
          exposed_sorts_label: 'Sort by'
 | 
			
		||||
          expose_sort_order: true
 | 
			
		||||
          sort_asc_label: Asc
 | 
			
		||||
          sort_desc_label: Desc
 | 
			
		||||
      access:
 | 
			
		||||
        type: perm
 | 
			
		||||
        options:
 | 
			
		||||
          perm: 'view all revisions'
 | 
			
		||||
      cache:
 | 
			
		||||
        type: tag
 | 
			
		||||
        options: {  }
 | 
			
		||||
      empty: {  }
 | 
			
		||||
      sorts:
 | 
			
		||||
        vid:
 | 
			
		||||
          id: vid
 | 
			
		||||
          table: node_field_revision
 | 
			
		||||
          field: vid
 | 
			
		||||
          relationship: none
 | 
			
		||||
          group_type: group
 | 
			
		||||
          admin_label: ''
 | 
			
		||||
          entity_type: node
 | 
			
		||||
          entity_field: vid
 | 
			
		||||
          plugin_id: standard
 | 
			
		||||
          order: ASC
 | 
			
		||||
          expose:
 | 
			
		||||
            label: ''
 | 
			
		||||
          exposed: false
 | 
			
		||||
      arguments: {  }
 | 
			
		||||
      filters:
 | 
			
		||||
        moderation_state:
 | 
			
		||||
          id: moderation_state
 | 
			
		||||
          table: node_field_revision
 | 
			
		||||
          field: moderation_state
 | 
			
		||||
          relationship: none
 | 
			
		||||
          group_type: group
 | 
			
		||||
          admin_label: ''
 | 
			
		||||
          entity_type: node
 | 
			
		||||
          plugin_id: moderation_state_filter
 | 
			
		||||
          operator: in
 | 
			
		||||
          value: {  }
 | 
			
		||||
          group: 1
 | 
			
		||||
          exposed: true
 | 
			
		||||
          expose:
 | 
			
		||||
            operator_id: moderation_state_op
 | 
			
		||||
            label: 'Moderation state'
 | 
			
		||||
            description: ''
 | 
			
		||||
            use_operator: false
 | 
			
		||||
            operator: moderation_state_op
 | 
			
		||||
            identifier: moderation_state
 | 
			
		||||
            required: false
 | 
			
		||||
            remember: false
 | 
			
		||||
            multiple: false
 | 
			
		||||
            remember_roles:
 | 
			
		||||
              authenticated: authenticated
 | 
			
		||||
            reduce: false
 | 
			
		||||
          is_grouped: false
 | 
			
		||||
          group_info:
 | 
			
		||||
            label: ''
 | 
			
		||||
            description: ''
 | 
			
		||||
            identifier: ''
 | 
			
		||||
            optional: true
 | 
			
		||||
            widget: select
 | 
			
		||||
            multiple: false
 | 
			
		||||
            remember: false
 | 
			
		||||
            default_group: All
 | 
			
		||||
            default_group_multiple: {  }
 | 
			
		||||
            group_items: {  }
 | 
			
		||||
      style:
 | 
			
		||||
        type: default
 | 
			
		||||
        options:
 | 
			
		||||
          grouping: {  }
 | 
			
		||||
          row_class: ''
 | 
			
		||||
          default_row_class: true
 | 
			
		||||
          uses_fields: false
 | 
			
		||||
      row:
 | 
			
		||||
        type: fields
 | 
			
		||||
        options:
 | 
			
		||||
          default_field_elements: true
 | 
			
		||||
          inline: {  }
 | 
			
		||||
          separator: ''
 | 
			
		||||
          hide_empty: false
 | 
			
		||||
      query:
 | 
			
		||||
        type: views_query
 | 
			
		||||
        options:
 | 
			
		||||
          query_comment: ''
 | 
			
		||||
          disable_sql_rewrite: false
 | 
			
		||||
          distinct: false
 | 
			
		||||
          replica: false
 | 
			
		||||
          query_tags: {  }
 | 
			
		||||
      relationships: {  }
 | 
			
		||||
      header: {  }
 | 
			
		||||
      footer: {  }
 | 
			
		||||
      display_extenders: {  }
 | 
			
		||||
    cache_metadata:
 | 
			
		||||
      max-age: -1
 | 
			
		||||
      contexts:
 | 
			
		||||
        - 'languages:language_content'
 | 
			
		||||
        - 'languages:language_interface'
 | 
			
		||||
        - url
 | 
			
		||||
        - url.query_args
 | 
			
		||||
        - 'user.node_grants:view'
 | 
			
		||||
        - user.permissions
 | 
			
		||||
      tags: {  }
 | 
			
		||||
@ -0,0 +1,266 @@
 | 
			
		||||
langcode: en
 | 
			
		||||
status: true
 | 
			
		||||
dependencies:
 | 
			
		||||
  module:
 | 
			
		||||
    - content_moderation
 | 
			
		||||
    - node
 | 
			
		||||
    - user
 | 
			
		||||
id: test_content_moderation_state_sort_base_table
 | 
			
		||||
label: test_content_moderation_state_sort_base_table
 | 
			
		||||
module: views
 | 
			
		||||
description: ''
 | 
			
		||||
tag: ''
 | 
			
		||||
base_table: node_field_data
 | 
			
		||||
base_field: nid
 | 
			
		||||
display:
 | 
			
		||||
  default:
 | 
			
		||||
    id: default
 | 
			
		||||
    display_title: Default
 | 
			
		||||
    display_plugin: default
 | 
			
		||||
    position: 0
 | 
			
		||||
    display_options:
 | 
			
		||||
      fields:
 | 
			
		||||
        nid:
 | 
			
		||||
          id: nid
 | 
			
		||||
          table: node_field_data
 | 
			
		||||
          field: nid
 | 
			
		||||
          relationship: none
 | 
			
		||||
          group_type: group
 | 
			
		||||
          admin_label: ''
 | 
			
		||||
          entity_type: node
 | 
			
		||||
          entity_field: nid
 | 
			
		||||
          plugin_id: field
 | 
			
		||||
          label: ''
 | 
			
		||||
          exclude: false
 | 
			
		||||
          alter:
 | 
			
		||||
            alter_text: false
 | 
			
		||||
            text: ''
 | 
			
		||||
            make_link: false
 | 
			
		||||
            path: ''
 | 
			
		||||
            absolute: false
 | 
			
		||||
            external: false
 | 
			
		||||
            replace_spaces: false
 | 
			
		||||
            path_case: none
 | 
			
		||||
            trim_whitespace: false
 | 
			
		||||
            alt: ''
 | 
			
		||||
            rel: ''
 | 
			
		||||
            link_class: ''
 | 
			
		||||
            prefix: ''
 | 
			
		||||
            suffix: ''
 | 
			
		||||
            target: ''
 | 
			
		||||
            nl2br: false
 | 
			
		||||
            max_length: 0
 | 
			
		||||
            word_boundary: true
 | 
			
		||||
            ellipsis: true
 | 
			
		||||
            more_link: false
 | 
			
		||||
            more_link_text: ''
 | 
			
		||||
            more_link_path: ''
 | 
			
		||||
            strip_tags: false
 | 
			
		||||
            trim: false
 | 
			
		||||
            preserve_tags: ''
 | 
			
		||||
            html: false
 | 
			
		||||
          element_type: ''
 | 
			
		||||
          element_class: ''
 | 
			
		||||
          element_label_type: ''
 | 
			
		||||
          element_label_class: ''
 | 
			
		||||
          element_label_colon: false
 | 
			
		||||
          element_wrapper_type: ''
 | 
			
		||||
          element_wrapper_class: ''
 | 
			
		||||
          element_default_classes: true
 | 
			
		||||
          empty: ''
 | 
			
		||||
          hide_empty: false
 | 
			
		||||
          empty_zero: false
 | 
			
		||||
          hide_alter_empty: true
 | 
			
		||||
          click_sort_column: value
 | 
			
		||||
          type: number_integer
 | 
			
		||||
          settings:
 | 
			
		||||
            thousand_separator: ''
 | 
			
		||||
            prefix_suffix: true
 | 
			
		||||
          group_column: value
 | 
			
		||||
          group_columns: {  }
 | 
			
		||||
          group_rows: true
 | 
			
		||||
          delta_limit: 0
 | 
			
		||||
          delta_offset: 0
 | 
			
		||||
          delta_reversed: false
 | 
			
		||||
          delta_first_last: false
 | 
			
		||||
          multi_type: separator
 | 
			
		||||
          separator: ', '
 | 
			
		||||
          field_api_classes: false
 | 
			
		||||
        moderation_state:
 | 
			
		||||
          id: moderation_state
 | 
			
		||||
          table: node_field_data
 | 
			
		||||
          field: moderation_state
 | 
			
		||||
          relationship: none
 | 
			
		||||
          group_type: group
 | 
			
		||||
          admin_label: ''
 | 
			
		||||
          entity_type: node
 | 
			
		||||
          plugin_id: moderation_state_field
 | 
			
		||||
          label: 'Moderation state'
 | 
			
		||||
          exclude: false
 | 
			
		||||
          alter:
 | 
			
		||||
            alter_text: false
 | 
			
		||||
            text: ''
 | 
			
		||||
            make_link: false
 | 
			
		||||
            path: ''
 | 
			
		||||
            absolute: false
 | 
			
		||||
            external: false
 | 
			
		||||
            replace_spaces: false
 | 
			
		||||
            path_case: none
 | 
			
		||||
            trim_whitespace: false
 | 
			
		||||
            alt: ''
 | 
			
		||||
            rel: ''
 | 
			
		||||
            link_class: ''
 | 
			
		||||
            prefix: ''
 | 
			
		||||
            suffix: ''
 | 
			
		||||
            target: ''
 | 
			
		||||
            nl2br: false
 | 
			
		||||
            max_length: 0
 | 
			
		||||
            word_boundary: true
 | 
			
		||||
            ellipsis: true
 | 
			
		||||
            more_link: false
 | 
			
		||||
            more_link_text: ''
 | 
			
		||||
            more_link_path: ''
 | 
			
		||||
            strip_tags: false
 | 
			
		||||
            trim: false
 | 
			
		||||
            preserve_tags: ''
 | 
			
		||||
            html: false
 | 
			
		||||
          element_type: ''
 | 
			
		||||
          element_class: ''
 | 
			
		||||
          element_label_type: ''
 | 
			
		||||
          element_label_class: ''
 | 
			
		||||
          element_label_colon: true
 | 
			
		||||
          element_wrapper_type: ''
 | 
			
		||||
          element_wrapper_class: ''
 | 
			
		||||
          element_default_classes: true
 | 
			
		||||
          empty: ''
 | 
			
		||||
          hide_empty: false
 | 
			
		||||
          empty_zero: false
 | 
			
		||||
          hide_alter_empty: true
 | 
			
		||||
          click_sort_column: value
 | 
			
		||||
          type: content_moderation_state
 | 
			
		||||
          settings: {  }
 | 
			
		||||
          group_column: value
 | 
			
		||||
          group_columns: {  }
 | 
			
		||||
          group_rows: true
 | 
			
		||||
          delta_limit: 0
 | 
			
		||||
          delta_offset: 0
 | 
			
		||||
          delta_reversed: false
 | 
			
		||||
          delta_first_last: false
 | 
			
		||||
          multi_type: separator
 | 
			
		||||
          separator: ', '
 | 
			
		||||
          field_api_classes: false
 | 
			
		||||
      pager:
 | 
			
		||||
        type: mini
 | 
			
		||||
        options:
 | 
			
		||||
          offset: 0
 | 
			
		||||
          items_per_page: 10
 | 
			
		||||
          total_pages: null
 | 
			
		||||
          id: 0
 | 
			
		||||
          tags:
 | 
			
		||||
            next: ››
 | 
			
		||||
            previous: ‹‹
 | 
			
		||||
          expose:
 | 
			
		||||
            items_per_page: false
 | 
			
		||||
            items_per_page_label: 'Items per page'
 | 
			
		||||
            items_per_page_options: '5, 10, 25, 50'
 | 
			
		||||
            items_per_page_options_all: false
 | 
			
		||||
            items_per_page_options_all_label: '- All -'
 | 
			
		||||
            offset: false
 | 
			
		||||
            offset_label: Offset
 | 
			
		||||
      exposed_form:
 | 
			
		||||
        type: basic
 | 
			
		||||
        options:
 | 
			
		||||
          submit_button: Apply
 | 
			
		||||
          reset_button: false
 | 
			
		||||
          reset_button_label: Reset
 | 
			
		||||
          exposed_sorts_label: 'Sort by'
 | 
			
		||||
          expose_sort_order: true
 | 
			
		||||
          sort_asc_label: Asc
 | 
			
		||||
          sort_desc_label: Desc
 | 
			
		||||
      access:
 | 
			
		||||
        type: perm
 | 
			
		||||
        options:
 | 
			
		||||
          perm: 'access content'
 | 
			
		||||
      cache:
 | 
			
		||||
        type: tag
 | 
			
		||||
        options: {  }
 | 
			
		||||
      empty: {  }
 | 
			
		||||
      sorts:
 | 
			
		||||
        moderation_state:
 | 
			
		||||
          id: moderation_state
 | 
			
		||||
          table: node_field_data
 | 
			
		||||
          field: moderation_state
 | 
			
		||||
          relationship: none
 | 
			
		||||
          group_type: group
 | 
			
		||||
          admin_label: ''
 | 
			
		||||
          entity_type: node
 | 
			
		||||
          plugin_id: moderation_state_sort
 | 
			
		||||
          order: ASC
 | 
			
		||||
          expose:
 | 
			
		||||
            label: 'Moderation state'
 | 
			
		||||
            field_identifier: moderation_state
 | 
			
		||||
          exposed: true
 | 
			
		||||
      arguments: {  }
 | 
			
		||||
      filters: {  }
 | 
			
		||||
      style:
 | 
			
		||||
        type: table
 | 
			
		||||
        options:
 | 
			
		||||
          grouping: {  }
 | 
			
		||||
          class: ''
 | 
			
		||||
          row_class: ''
 | 
			
		||||
          default_row_class: true
 | 
			
		||||
          columns:
 | 
			
		||||
            nid: nid
 | 
			
		||||
            moderation_state: moderation_state
 | 
			
		||||
          default: '-1'
 | 
			
		||||
          info:
 | 
			
		||||
            nid:
 | 
			
		||||
              sortable: false
 | 
			
		||||
              default_sort_order: asc
 | 
			
		||||
              align: ''
 | 
			
		||||
              separator: ''
 | 
			
		||||
              empty_column: false
 | 
			
		||||
              responsive: ''
 | 
			
		||||
            moderation_state:
 | 
			
		||||
              sortable: true
 | 
			
		||||
              default_sort_order: asc
 | 
			
		||||
              align: ''
 | 
			
		||||
              separator: ''
 | 
			
		||||
              empty_column: false
 | 
			
		||||
              responsive: ''
 | 
			
		||||
          override: true
 | 
			
		||||
          sticky: false
 | 
			
		||||
          summary: ''
 | 
			
		||||
          empty_table: false
 | 
			
		||||
          caption: ''
 | 
			
		||||
          description: ''
 | 
			
		||||
      row:
 | 
			
		||||
        type: fields
 | 
			
		||||
        options:
 | 
			
		||||
          default_field_elements: true
 | 
			
		||||
          inline: {  }
 | 
			
		||||
          separator: ''
 | 
			
		||||
          hide_empty: false
 | 
			
		||||
      query:
 | 
			
		||||
        type: views_query
 | 
			
		||||
        options:
 | 
			
		||||
          query_comment: ''
 | 
			
		||||
          disable_sql_rewrite: false
 | 
			
		||||
          distinct: false
 | 
			
		||||
          replica: false
 | 
			
		||||
          query_tags: {  }
 | 
			
		||||
      relationships: {  }
 | 
			
		||||
      header: {  }
 | 
			
		||||
      footer: {  }
 | 
			
		||||
      display_extenders: {  }
 | 
			
		||||
    cache_metadata:
 | 
			
		||||
      max-age: -1
 | 
			
		||||
      contexts:
 | 
			
		||||
        - 'languages:language_content'
 | 
			
		||||
        - 'languages:language_interface'
 | 
			
		||||
        - url.query_args
 | 
			
		||||
        - 'url.query_args:sort_by'
 | 
			
		||||
        - 'url.query_args:sort_order'
 | 
			
		||||
        - 'user.node_grants:view'
 | 
			
		||||
        - user.permissions
 | 
			
		||||
      tags: {  }
 | 
			
		||||
@ -0,0 +1,265 @@
 | 
			
		||||
langcode: en
 | 
			
		||||
status: true
 | 
			
		||||
dependencies:
 | 
			
		||||
  module:
 | 
			
		||||
    - content_moderation
 | 
			
		||||
    - user
 | 
			
		||||
id: test_content_moderation_state_sort_revision_table
 | 
			
		||||
label: test_content_moderation_state_sort_revision_table
 | 
			
		||||
module: views
 | 
			
		||||
description: ''
 | 
			
		||||
tag: ''
 | 
			
		||||
base_table: node_field_revision
 | 
			
		||||
base_field: vid
 | 
			
		||||
display:
 | 
			
		||||
  default:
 | 
			
		||||
    id: default
 | 
			
		||||
    display_title: Default
 | 
			
		||||
    display_plugin: default
 | 
			
		||||
    position: 0
 | 
			
		||||
    display_options:
 | 
			
		||||
      fields:
 | 
			
		||||
        vid:
 | 
			
		||||
          id: vid
 | 
			
		||||
          table: node_field_revision
 | 
			
		||||
          field: vid
 | 
			
		||||
          relationship: none
 | 
			
		||||
          group_type: group
 | 
			
		||||
          admin_label: ''
 | 
			
		||||
          entity_type: node
 | 
			
		||||
          entity_field: vid
 | 
			
		||||
          plugin_id: field
 | 
			
		||||
          label: ''
 | 
			
		||||
          exclude: false
 | 
			
		||||
          alter:
 | 
			
		||||
            alter_text: false
 | 
			
		||||
            text: ''
 | 
			
		||||
            make_link: false
 | 
			
		||||
            path: ''
 | 
			
		||||
            absolute: false
 | 
			
		||||
            external: false
 | 
			
		||||
            replace_spaces: false
 | 
			
		||||
            path_case: none
 | 
			
		||||
            trim_whitespace: false
 | 
			
		||||
            alt: ''
 | 
			
		||||
            rel: ''
 | 
			
		||||
            link_class: ''
 | 
			
		||||
            prefix: ''
 | 
			
		||||
            suffix: ''
 | 
			
		||||
            target: ''
 | 
			
		||||
            nl2br: false
 | 
			
		||||
            max_length: 0
 | 
			
		||||
            word_boundary: true
 | 
			
		||||
            ellipsis: true
 | 
			
		||||
            more_link: false
 | 
			
		||||
            more_link_text: ''
 | 
			
		||||
            more_link_path: ''
 | 
			
		||||
            strip_tags: false
 | 
			
		||||
            trim: false
 | 
			
		||||
            preserve_tags: ''
 | 
			
		||||
            html: false
 | 
			
		||||
          element_type: ''
 | 
			
		||||
          element_class: ''
 | 
			
		||||
          element_label_type: ''
 | 
			
		||||
          element_label_class: ''
 | 
			
		||||
          element_label_colon: false
 | 
			
		||||
          element_wrapper_type: ''
 | 
			
		||||
          element_wrapper_class: ''
 | 
			
		||||
          element_default_classes: true
 | 
			
		||||
          empty: ''
 | 
			
		||||
          hide_empty: false
 | 
			
		||||
          empty_zero: false
 | 
			
		||||
          hide_alter_empty: true
 | 
			
		||||
          click_sort_column: value
 | 
			
		||||
          type: number_integer
 | 
			
		||||
          settings:
 | 
			
		||||
            thousand_separator: ''
 | 
			
		||||
            prefix_suffix: true
 | 
			
		||||
          group_column: value
 | 
			
		||||
          group_columns: {  }
 | 
			
		||||
          group_rows: true
 | 
			
		||||
          delta_limit: 0
 | 
			
		||||
          delta_offset: 0
 | 
			
		||||
          delta_reversed: false
 | 
			
		||||
          delta_first_last: false
 | 
			
		||||
          multi_type: separator
 | 
			
		||||
          separator: ', '
 | 
			
		||||
          field_api_classes: false
 | 
			
		||||
        moderation_state:
 | 
			
		||||
          id: moderation_state
 | 
			
		||||
          table: node_field_revision
 | 
			
		||||
          field: moderation_state
 | 
			
		||||
          relationship: none
 | 
			
		||||
          group_type: group
 | 
			
		||||
          admin_label: ''
 | 
			
		||||
          entity_type: node
 | 
			
		||||
          plugin_id: moderation_state_field
 | 
			
		||||
          label: 'Moderation state'
 | 
			
		||||
          exclude: false
 | 
			
		||||
          alter:
 | 
			
		||||
            alter_text: false
 | 
			
		||||
            text: ''
 | 
			
		||||
            make_link: false
 | 
			
		||||
            path: ''
 | 
			
		||||
            absolute: false
 | 
			
		||||
            external: false
 | 
			
		||||
            replace_spaces: false
 | 
			
		||||
            path_case: none
 | 
			
		||||
            trim_whitespace: false
 | 
			
		||||
            alt: ''
 | 
			
		||||
            rel: ''
 | 
			
		||||
            link_class: ''
 | 
			
		||||
            prefix: ''
 | 
			
		||||
            suffix: ''
 | 
			
		||||
            target: ''
 | 
			
		||||
            nl2br: false
 | 
			
		||||
            max_length: 0
 | 
			
		||||
            word_boundary: true
 | 
			
		||||
            ellipsis: true
 | 
			
		||||
            more_link: false
 | 
			
		||||
            more_link_text: ''
 | 
			
		||||
            more_link_path: ''
 | 
			
		||||
            strip_tags: false
 | 
			
		||||
            trim: false
 | 
			
		||||
            preserve_tags: ''
 | 
			
		||||
            html: false
 | 
			
		||||
          element_type: ''
 | 
			
		||||
          element_class: ''
 | 
			
		||||
          element_label_type: ''
 | 
			
		||||
          element_label_class: ''
 | 
			
		||||
          element_label_colon: true
 | 
			
		||||
          element_wrapper_type: ''
 | 
			
		||||
          element_wrapper_class: ''
 | 
			
		||||
          element_default_classes: true
 | 
			
		||||
          empty: ''
 | 
			
		||||
          hide_empty: false
 | 
			
		||||
          empty_zero: false
 | 
			
		||||
          hide_alter_empty: true
 | 
			
		||||
          click_sort_column: value
 | 
			
		||||
          type: content_moderation_state
 | 
			
		||||
          settings: {  }
 | 
			
		||||
          group_column: value
 | 
			
		||||
          group_columns: {  }
 | 
			
		||||
          group_rows: true
 | 
			
		||||
          delta_limit: 0
 | 
			
		||||
          delta_offset: 0
 | 
			
		||||
          delta_reversed: false
 | 
			
		||||
          delta_first_last: false
 | 
			
		||||
          multi_type: separator
 | 
			
		||||
          separator: ', '
 | 
			
		||||
          field_api_classes: false
 | 
			
		||||
      pager:
 | 
			
		||||
        type: mini
 | 
			
		||||
        options:
 | 
			
		||||
          offset: 0
 | 
			
		||||
          items_per_page: 10
 | 
			
		||||
          total_pages: null
 | 
			
		||||
          id: 0
 | 
			
		||||
          tags:
 | 
			
		||||
            next: ››
 | 
			
		||||
            previous: ‹‹
 | 
			
		||||
          expose:
 | 
			
		||||
            items_per_page: false
 | 
			
		||||
            items_per_page_label: 'Items per page'
 | 
			
		||||
            items_per_page_options: '5, 10, 25, 50'
 | 
			
		||||
            items_per_page_options_all: false
 | 
			
		||||
            items_per_page_options_all_label: '- All -'
 | 
			
		||||
            offset: false
 | 
			
		||||
            offset_label: Offset
 | 
			
		||||
      exposed_form:
 | 
			
		||||
        type: basic
 | 
			
		||||
        options:
 | 
			
		||||
          submit_button: Apply
 | 
			
		||||
          reset_button: false
 | 
			
		||||
          reset_button_label: Reset
 | 
			
		||||
          exposed_sorts_label: 'Sort by'
 | 
			
		||||
          expose_sort_order: true
 | 
			
		||||
          sort_asc_label: Asc
 | 
			
		||||
          sort_desc_label: Desc
 | 
			
		||||
      access:
 | 
			
		||||
        type: perm
 | 
			
		||||
        options:
 | 
			
		||||
          perm: 'view all revisions'
 | 
			
		||||
      cache:
 | 
			
		||||
        type: tag
 | 
			
		||||
        options: {  }
 | 
			
		||||
      empty: {  }
 | 
			
		||||
      sorts:
 | 
			
		||||
        moderation_state:
 | 
			
		||||
          id: moderation_state
 | 
			
		||||
          table: node_field_revision
 | 
			
		||||
          field: moderation_state
 | 
			
		||||
          relationship: none
 | 
			
		||||
          group_type: group
 | 
			
		||||
          admin_label: ''
 | 
			
		||||
          entity_type: node
 | 
			
		||||
          plugin_id: moderation_state_sort
 | 
			
		||||
          order: ASC
 | 
			
		||||
          expose:
 | 
			
		||||
            label: 'Moderation state'
 | 
			
		||||
            field_identifier: moderation_state
 | 
			
		||||
          exposed: true
 | 
			
		||||
      arguments: {  }
 | 
			
		||||
      filters: {  }
 | 
			
		||||
      style:
 | 
			
		||||
        type: table
 | 
			
		||||
        options:
 | 
			
		||||
          grouping: {  }
 | 
			
		||||
          class: ''
 | 
			
		||||
          row_class: ''
 | 
			
		||||
          default_row_class: true
 | 
			
		||||
          columns:
 | 
			
		||||
            vid: vid
 | 
			
		||||
            moderation_state: moderation_state
 | 
			
		||||
          default: '-1'
 | 
			
		||||
          info:
 | 
			
		||||
            vid:
 | 
			
		||||
              sortable: false
 | 
			
		||||
              default_sort_order: asc
 | 
			
		||||
              align: ''
 | 
			
		||||
              separator: ''
 | 
			
		||||
              empty_column: false
 | 
			
		||||
              responsive: ''
 | 
			
		||||
            moderation_state:
 | 
			
		||||
              sortable: true
 | 
			
		||||
              default_sort_order: asc
 | 
			
		||||
              align: ''
 | 
			
		||||
              separator: ''
 | 
			
		||||
              empty_column: false
 | 
			
		||||
              responsive: ''
 | 
			
		||||
          override: true
 | 
			
		||||
          sticky: false
 | 
			
		||||
          summary: ''
 | 
			
		||||
          empty_table: false
 | 
			
		||||
          caption: ''
 | 
			
		||||
          description: ''
 | 
			
		||||
      row:
 | 
			
		||||
        type: fields
 | 
			
		||||
        options:
 | 
			
		||||
          default_field_elements: true
 | 
			
		||||
          inline: {  }
 | 
			
		||||
          separator: ''
 | 
			
		||||
          hide_empty: false
 | 
			
		||||
      query:
 | 
			
		||||
        type: views_query
 | 
			
		||||
        options:
 | 
			
		||||
          query_comment: ''
 | 
			
		||||
          disable_sql_rewrite: false
 | 
			
		||||
          distinct: false
 | 
			
		||||
          replica: false
 | 
			
		||||
          query_tags: {  }
 | 
			
		||||
      relationships: {  }
 | 
			
		||||
      header: {  }
 | 
			
		||||
      footer: {  }
 | 
			
		||||
      display_extenders: {  }
 | 
			
		||||
    cache_metadata:
 | 
			
		||||
      max-age: -1
 | 
			
		||||
      contexts:
 | 
			
		||||
        - 'languages:language_content'
 | 
			
		||||
        - 'languages:language_interface'
 | 
			
		||||
        - url.query_args
 | 
			
		||||
        - 'url.query_args:sort_by'
 | 
			
		||||
        - 'url.query_args:sort_order'
 | 
			
		||||
        - 'user.node_grants:view'
 | 
			
		||||
        - user.permissions
 | 
			
		||||
      tags: {  }
 | 
			
		||||
@ -0,0 +1,10 @@
 | 
			
		||||
name: 'Content moderation test views'
 | 
			
		||||
type: module
 | 
			
		||||
description: 'Provides default views for views Content moderation tests.'
 | 
			
		||||
package: Testing
 | 
			
		||||
version: VERSION
 | 
			
		||||
dependencies:
 | 
			
		||||
  - drupal:content_moderation
 | 
			
		||||
  - drupal:node
 | 
			
		||||
  - drupal:views
 | 
			
		||||
  - drupal:entity_test
 | 
			
		||||
@ -0,0 +1,54 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\content_moderation_test_views\Hook;
 | 
			
		||||
 | 
			
		||||
use Drupal\views\Plugin\views\query\QueryPluginBase;
 | 
			
		||||
use Drupal\views\ViewExecutable;
 | 
			
		||||
use Drupal\Core\Hook\Attribute\Hook;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Hook implementations for content_moderation_test_views.
 | 
			
		||||
 */
 | 
			
		||||
class ContentModerationTestViewsHooks {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Implements hook_views_query_alter().
 | 
			
		||||
   *
 | 
			
		||||
   * @see \Drupal\Tests\content_moderation\Kernel\ViewsModerationStateSortTest::testSortRevisionBaseTable()
 | 
			
		||||
   */
 | 
			
		||||
  #[Hook('views_query_alter')]
 | 
			
		||||
  public function viewsQueryAlter(ViewExecutable $view, QueryPluginBase $query): void {
 | 
			
		||||
    // Add a secondary sort order to ensure consistent builds when testing click
 | 
			
		||||
    // and table sorting.
 | 
			
		||||
    if ($view->id() === 'test_content_moderation_state_sort_revision_table') {
 | 
			
		||||
      $query->addOrderBy('node_field_revision', 'vid', 'ASC');
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Implements hook_views_data_alter().
 | 
			
		||||
   *
 | 
			
		||||
   * @see \Drupal\Tests\content_moderation\Kernel\ViewsModerationStateFilterTest
 | 
			
		||||
   */
 | 
			
		||||
  #[Hook('views_data_alter')]
 | 
			
		||||
  public function viewsDataAlter(array &$data): void {
 | 
			
		||||
    if (isset($data['users_field_data'])) {
 | 
			
		||||
      $data['users_field_data']['uid_revision_test'] = [
 | 
			
		||||
        'help' => 'Relate the content revision to the user who created it.',
 | 
			
		||||
        'real field' => 'uid',
 | 
			
		||||
        'relationship' => [
 | 
			
		||||
          'title' => 'Content revision authored',
 | 
			
		||||
          'help' => 'Relate the content revision to the user who created it. This relationship will create one record for each content revision item created by the user.',
 | 
			
		||||
          'id' => 'standard',
 | 
			
		||||
          'base' => 'node_field_revision',
 | 
			
		||||
          'base field' => 'uid',
 | 
			
		||||
          'field' => 'uid',
 | 
			
		||||
          'label' => 'node revisions',
 | 
			
		||||
        ],
 | 
			
		||||
      ];
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,30 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\content_moderation\Functional;
 | 
			
		||||
 | 
			
		||||
use Drupal\Tests\language\Functional\AdminPathEntityConverterLanguageTest;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Test administration path based entity conversion when moderation enabled.
 | 
			
		||||
 *
 | 
			
		||||
 * @group content_moderation
 | 
			
		||||
 */
 | 
			
		||||
class ContentModerationAdminPathEntityConverterLanguageTest extends AdminPathEntityConverterLanguageTest {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = [
 | 
			
		||||
    'language',
 | 
			
		||||
    'language_test',
 | 
			
		||||
    'content_moderation',
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,113 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\content_moderation\Functional;
 | 
			
		||||
 | 
			
		||||
use Drupal\Tests\BrowserTestBase;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Test the workflow type plugin in the content_moderation module.
 | 
			
		||||
 *
 | 
			
		||||
 * @group content_moderation
 | 
			
		||||
 */
 | 
			
		||||
class ContentModerationWorkflowTypeTest extends BrowserTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = [
 | 
			
		||||
    'content_moderation',
 | 
			
		||||
    'node',
 | 
			
		||||
    'entity_test',
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
    $admin = $this->drupalCreateUser([
 | 
			
		||||
      'administer workflows',
 | 
			
		||||
    ]);
 | 
			
		||||
    $this->drupalLogin($admin);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests creating a new workflow using the content moderation plugin.
 | 
			
		||||
   */
 | 
			
		||||
  public function testNewWorkflow(): void {
 | 
			
		||||
    $types[] = $this->createContentType();
 | 
			
		||||
    $types[] = $this->createContentType();
 | 
			
		||||
    $types[] = $this->createContentType();
 | 
			
		||||
 | 
			
		||||
    $entity_bundle_info = \Drupal::service('entity_type.bundle.info');
 | 
			
		||||
 | 
			
		||||
    $this->drupalGet('admin/config/workflow/workflows/add');
 | 
			
		||||
    $this->submitForm([
 | 
			
		||||
      'label' => 'Test',
 | 
			
		||||
      'id' => 'test',
 | 
			
		||||
      'workflow_type' => 'content_moderation',
 | 
			
		||||
    ], 'Save');
 | 
			
		||||
 | 
			
		||||
    $session = $this->assertSession();
 | 
			
		||||
    // Make sure the test workflow includes the default states and transitions.
 | 
			
		||||
    $session->pageTextContains('Draft');
 | 
			
		||||
    $session->pageTextContains('Published');
 | 
			
		||||
    $session->pageTextContains('Create New Draft');
 | 
			
		||||
    $session->pageTextContains('Publish');
 | 
			
		||||
 | 
			
		||||
    $session->linkByHrefNotExists('/admin/config/workflow/workflows/manage/test/state/draft/delete');
 | 
			
		||||
    $session->linkByHrefNotExists('/admin/config/workflow/workflows/manage/test/state/published/delete');
 | 
			
		||||
 | 
			
		||||
    // Ensure after a workflow is created, the bundle information can be
 | 
			
		||||
    // refreshed.
 | 
			
		||||
    $entity_bundle_info->clearCachedBundles();
 | 
			
		||||
    $this->assertNotEmpty($entity_bundle_info->getAllBundleInfo());
 | 
			
		||||
 | 
			
		||||
    $this->clickLink('Add a new state');
 | 
			
		||||
    $this->submitForm([
 | 
			
		||||
      'label' => 'Test State',
 | 
			
		||||
      'id' => 'test_state',
 | 
			
		||||
      'type_settings[published]' => TRUE,
 | 
			
		||||
      'type_settings[default_revision]' => FALSE,
 | 
			
		||||
    ], 'Save');
 | 
			
		||||
    $session->pageTextContains('Created Test State state.');
 | 
			
		||||
    $session->linkByHrefExists('/admin/config/workflow/workflows/manage/test/state/test_state/delete');
 | 
			
		||||
 | 
			
		||||
    // Check there is a link to delete a default transition.
 | 
			
		||||
    $session->linkByHrefExists('/admin/config/workflow/workflows/manage/test/transition/publish/delete');
 | 
			
		||||
    // Delete the transition.
 | 
			
		||||
    $this->drupalGet('/admin/config/workflow/workflows/manage/test/transition/publish/delete');
 | 
			
		||||
    $this->submitForm([], 'Delete');
 | 
			
		||||
    // The link to delete the transition should now be gone.
 | 
			
		||||
    $session->linkByHrefNotExists('/admin/config/workflow/workflows/manage/test/transition/publish/delete');
 | 
			
		||||
 | 
			
		||||
    // Ensure that the published settings cannot be changed.
 | 
			
		||||
    $this->drupalGet('admin/config/workflow/workflows/manage/test/state/published');
 | 
			
		||||
    $session->fieldDisabled('type_settings[published]');
 | 
			
		||||
    $session->fieldDisabled('type_settings[default_revision]');
 | 
			
		||||
 | 
			
		||||
    // Ensure that the draft settings cannot be changed.
 | 
			
		||||
    $this->drupalGet('admin/config/workflow/workflows/manage/test/state/draft');
 | 
			
		||||
    $session->fieldDisabled('type_settings[published]');
 | 
			
		||||
    $session->fieldDisabled('type_settings[default_revision]');
 | 
			
		||||
 | 
			
		||||
    $this->drupalGet('admin/config/workflow/workflows/manage/test/type/node');
 | 
			
		||||
    $session->pageTextContains('Select the content types for the Test workflow');
 | 
			
		||||
    foreach ($types as $type) {
 | 
			
		||||
      $session->pageTextContains($type->label());
 | 
			
		||||
      $session->elementContains('css', sprintf('.form-item-bundles-%s label', $type->id()), sprintf('Update %s', $type->label()));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Ensure warning message are displayed for unsupported features.
 | 
			
		||||
    $this->drupalGet('admin/config/workflow/workflows/manage/test/type/entity_test_rev');
 | 
			
		||||
    $this->assertSession()->pageTextContains('Test entity - revisions entities do not support publishing statuses. For example, even after transitioning from a published workflow state to an unpublished workflow state they will still be visible to site visitors.');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,61 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\content_moderation\Functional;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests setting a custom default moderation state.
 | 
			
		||||
 *
 | 
			
		||||
 * @group content_moderation
 | 
			
		||||
 */
 | 
			
		||||
class DefaultModerationStateTest extends ModerationStateTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
    $this->drupalLogin($this->adminUser);
 | 
			
		||||
    $this->createContentTypeFromUi('Moderated content', 'moderated_content', TRUE);
 | 
			
		||||
    $this->grantUserPermissionToCreateContentOfType($this->adminUser, 'moderated_content');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests a workflow with a default moderation state set.
 | 
			
		||||
   */
 | 
			
		||||
  public function testPublishedDefaultState(): void {
 | 
			
		||||
    // Set the default moderation state to be "published".
 | 
			
		||||
    $this->drupalGet('admin/config/workflow/workflows/manage/' . $this->workflow->id());
 | 
			
		||||
    $this->submitForm(['type_settings[workflow_settings][default_moderation_state]' => 'published'], 'Save');
 | 
			
		||||
 | 
			
		||||
    $this->drupalGet('node/add/moderated_content');
 | 
			
		||||
    $this->assertEquals('published', $this->assertSession()->selectExists('moderation_state[0][state]')->getValue());
 | 
			
		||||
    $this->submitForm([
 | 
			
		||||
      'title[0][value]' => 'moderated content',
 | 
			
		||||
    ], 'Save');
 | 
			
		||||
 | 
			
		||||
    $node = $this->getNodeByTitle('moderated content');
 | 
			
		||||
    $this->assertEquals('published', $node->moderation_state->value);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests access to deleting the default state.
 | 
			
		||||
   */
 | 
			
		||||
  public function testDeleteDefaultStateAccess(): void {
 | 
			
		||||
    $this->drupalGet('admin/config/workflow/workflows/manage/editorial/state/archived/delete');
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
 | 
			
		||||
    $this->drupalGet('admin/config/workflow/workflows/manage/' . $this->workflow->id());
 | 
			
		||||
    $this->submitForm(['type_settings[workflow_settings][default_moderation_state]' => 'archived'], 'Save');
 | 
			
		||||
 | 
			
		||||
    $this->drupalGet('admin/config/workflow/workflows/manage/editorial/state/archived/delete');
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(403);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,14 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\content_moderation\Functional;
 | 
			
		||||
 | 
			
		||||
use Drupal\Tests\system\Functional\Module\GenericModuleTestBase;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Generic module test for content_moderation.
 | 
			
		||||
 *
 | 
			
		||||
 * @group content_moderation
 | 
			
		||||
 */
 | 
			
		||||
class GenericTest extends GenericModuleTestBase {}
 | 
			
		||||
@ -0,0 +1,209 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\content_moderation\Functional;
 | 
			
		||||
 | 
			
		||||
use Drupal\block_content\Entity\BlockContentType;
 | 
			
		||||
use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay;
 | 
			
		||||
use Drupal\Tests\BrowserTestBase;
 | 
			
		||||
use Drupal\Tests\content_moderation\Traits\ContentModerationTestTrait;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests Content Moderation's integration with Layout Builder.
 | 
			
		||||
 *
 | 
			
		||||
 * @group content_moderation
 | 
			
		||||
 * @group layout_builder
 | 
			
		||||
 */
 | 
			
		||||
class LayoutBuilderContentModerationIntegrationTest extends BrowserTestBase {
 | 
			
		||||
 | 
			
		||||
  use ContentModerationTestTrait;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = [
 | 
			
		||||
    'layout_builder',
 | 
			
		||||
    'node',
 | 
			
		||||
    'content_moderation',
 | 
			
		||||
    'menu_ui',
 | 
			
		||||
    'block_content',
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
 | 
			
		||||
    // @todo The Layout Builder UI relies on local tasks; fix in
 | 
			
		||||
    //   https://www.drupal.org/project/drupal/issues/2917777.
 | 
			
		||||
    $this->drupalPlaceBlock('local_tasks_block');
 | 
			
		||||
 | 
			
		||||
    // Add a new bundle.
 | 
			
		||||
    $this->createContentType(['type' => 'bundle_with_section_field']);
 | 
			
		||||
 | 
			
		||||
    // Add a new block content bundle to the editorial workflow.
 | 
			
		||||
    BlockContentType::create([
 | 
			
		||||
      'id' => 'basic',
 | 
			
		||||
      'label' => 'Basic',
 | 
			
		||||
      'revision' => 1,
 | 
			
		||||
    ])->save();
 | 
			
		||||
    block_content_add_body_field('basic');
 | 
			
		||||
 | 
			
		||||
    // Enable layout overrides.
 | 
			
		||||
    LayoutBuilderEntityViewDisplay::load('node.bundle_with_section_field.default')
 | 
			
		||||
      ->enableLayoutBuilder()
 | 
			
		||||
      ->setOverridable()
 | 
			
		||||
      ->save();
 | 
			
		||||
    // Create a node before enabling the workflow on the bundle.
 | 
			
		||||
    $node = $this->createNode([
 | 
			
		||||
      'type' => 'bundle_with_section_field',
 | 
			
		||||
      'title' => 'Pre-workflow node',
 | 
			
		||||
      'body' => [
 | 
			
		||||
        [
 | 
			
		||||
          'value' => 'The first node body',
 | 
			
		||||
        ],
 | 
			
		||||
      ],
 | 
			
		||||
    ]);
 | 
			
		||||
    // View the node to ensure the new extra field blocks are not cached when
 | 
			
		||||
    // the workflow is updated.
 | 
			
		||||
    $this->drupalGet($node->toUrl());
 | 
			
		||||
    // Add editorial workflow for the bundle.
 | 
			
		||||
    $workflow = $this->createEditorialWorkflow();
 | 
			
		||||
    $workflow->getTypePlugin()->addEntityTypeAndBundle('block_content', 'basic');
 | 
			
		||||
    $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'bundle_with_section_field');
 | 
			
		||||
    $workflow->save();
 | 
			
		||||
 | 
			
		||||
    $this->drupalLogin($this->drupalCreateUser([
 | 
			
		||||
      'configure any layout',
 | 
			
		||||
      'edit any bundle_with_section_field content',
 | 
			
		||||
      'view bundle_with_section_field revisions',
 | 
			
		||||
      'revert bundle_with_section_field revisions',
 | 
			
		||||
      'view own unpublished content',
 | 
			
		||||
      'view latest version',
 | 
			
		||||
      'use editorial transition create_new_draft',
 | 
			
		||||
      'use editorial transition publish',
 | 
			
		||||
      'create and edit custom blocks',
 | 
			
		||||
    ]));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests that Layout changes are respected by Content Moderation.
 | 
			
		||||
   */
 | 
			
		||||
  public function testLayoutModeration(): void {
 | 
			
		||||
    $page = $this->getSession()->getPage();
 | 
			
		||||
    $assert_session = $this->assertSession();
 | 
			
		||||
 | 
			
		||||
    // Create an unpublished node. Revision count: 1.
 | 
			
		||||
    $node = $this->createNode([
 | 
			
		||||
      'type' => 'bundle_with_section_field',
 | 
			
		||||
      'title' => 'The first node title',
 | 
			
		||||
      'body' => [
 | 
			
		||||
        [
 | 
			
		||||
          'value' => 'The first node body',
 | 
			
		||||
        ],
 | 
			
		||||
      ],
 | 
			
		||||
    ]);
 | 
			
		||||
    _menu_ui_node_save($node, [
 | 
			
		||||
      'title' => 'bar',
 | 
			
		||||
      'menu_name' => 'main',
 | 
			
		||||
      'description' => 'view bar',
 | 
			
		||||
      'parent' => '',
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    $this->drupalGet($node->toUrl());
 | 
			
		||||
    // Publish the node. Revision count: 2.
 | 
			
		||||
    $page->fillField('new_state', 'published');
 | 
			
		||||
    $page->pressButton('Apply');
 | 
			
		||||
 | 
			
		||||
    // Modify the layout.
 | 
			
		||||
    $page->clickLink('Layout');
 | 
			
		||||
    $assert_session->checkboxChecked('revision');
 | 
			
		||||
    $assert_session->fieldDisabled('revision');
 | 
			
		||||
 | 
			
		||||
    $page->clickLink('Add block');
 | 
			
		||||
    $page->clickLink('Powered by Drupal');
 | 
			
		||||
    $page->pressButton('Add block');
 | 
			
		||||
    // Save the node as a draft. Revision count: 3.
 | 
			
		||||
    $page->fillField('moderation_state[0][state]', 'draft');
 | 
			
		||||
    $page->pressButton('Save layout');
 | 
			
		||||
 | 
			
		||||
    // Block is visible on the revision page.
 | 
			
		||||
    $assert_session->addressEquals("node/{$node->id()}/latest");
 | 
			
		||||
    $assert_session->pageTextContains('Powered by Drupal');
 | 
			
		||||
 | 
			
		||||
    // Block is visible on the layout form.
 | 
			
		||||
    $page->clickLink('Layout');
 | 
			
		||||
    $assert_session->pageTextContains('Powered by Drupal');
 | 
			
		||||
 | 
			
		||||
    // Block is not visible on the live node page.
 | 
			
		||||
    $page->clickLink('View');
 | 
			
		||||
    $assert_session->pageTextNotContains('Powered by Drupal');
 | 
			
		||||
 | 
			
		||||
    // Publish the node. Revision count: 4.
 | 
			
		||||
    $page->clickLink('Latest version');
 | 
			
		||||
    $page->fillField('new_state', 'published');
 | 
			
		||||
    $page->pressButton('Apply');
 | 
			
		||||
 | 
			
		||||
    // Block is visible on the live node page.
 | 
			
		||||
    $assert_session->pageTextContains('Powered by Drupal');
 | 
			
		||||
 | 
			
		||||
    // Revert to the previous revision.
 | 
			
		||||
    $page->clickLink('Revisions');
 | 
			
		||||
    // Assert that there are 4 total revisions and 3 revert links.
 | 
			
		||||
    $assert_session->elementsCount('named', ['link', 'Revert'], 3);
 | 
			
		||||
    // Revert to the 2nd revision before modifying the layout.
 | 
			
		||||
    $this->clickLink('Revert', 1);
 | 
			
		||||
    $page->pressButton('Revert');
 | 
			
		||||
 | 
			
		||||
    $page->clickLink('View');
 | 
			
		||||
    $assert_session->pageTextNotContains('Powered by Drupal');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Test placing inline blocks that belong to a moderated content block bundle.
 | 
			
		||||
   */
 | 
			
		||||
  public function testModeratedInlineBlockBundles(): void {
 | 
			
		||||
    $page = $this->getSession()->getPage();
 | 
			
		||||
    $assert_session = $this->assertSession();
 | 
			
		||||
 | 
			
		||||
    $node = $this->createNode([
 | 
			
		||||
      'type' => 'bundle_with_section_field',
 | 
			
		||||
      'title' => 'The first node title',
 | 
			
		||||
      'moderation_state' => 'published',
 | 
			
		||||
    ]);
 | 
			
		||||
    $this->drupalGet("node/{$node->id()}/layout");
 | 
			
		||||
    $page->clickLink('Add block');
 | 
			
		||||
    $this->clickLink('Create content block');
 | 
			
		||||
 | 
			
		||||
    $assert_session->fieldNotExists('settings[block_form][moderation_state][0][state]');
 | 
			
		||||
    $this->submitForm([
 | 
			
		||||
      'settings[label]' => 'Test inline block',
 | 
			
		||||
      'settings[block_form][body][0][value]' => 'Example block body',
 | 
			
		||||
    ], 'Add block');
 | 
			
		||||
 | 
			
		||||
    // Save a draft of the page with the inline block and ensure the drafted
 | 
			
		||||
    // content appears on the latest version page.
 | 
			
		||||
    $this->assertSession()->pageTextContains('Example block body');
 | 
			
		||||
    $this->submitForm([
 | 
			
		||||
      'moderation_state[0][state]' => 'draft',
 | 
			
		||||
    ], 'Save layout');
 | 
			
		||||
    $assert_session->pageTextContains('The layout override has been saved.');
 | 
			
		||||
    $assert_session->pageTextContains('Example block body');
 | 
			
		||||
 | 
			
		||||
    // Publish the draft of the page ensure the draft inline block content
 | 
			
		||||
    // appears on the published page.
 | 
			
		||||
    $this->submitForm([
 | 
			
		||||
      'new_state' => 'published',
 | 
			
		||||
    ], 'Apply');
 | 
			
		||||
    $assert_session->pageTextContains('The moderation state has been updated.');
 | 
			
		||||
    $assert_session->pageTextContains('Example block body');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,72 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\content_moderation\Functional;
 | 
			
		||||
 | 
			
		||||
use Drupal\Tests\BrowserTestBase;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests moderated content dynamic local task.
 | 
			
		||||
 *
 | 
			
		||||
 * @group content_moderation
 | 
			
		||||
 */
 | 
			
		||||
class ModeratedContentLocalTaskTest extends BrowserTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * A user to test with.
 | 
			
		||||
   *
 | 
			
		||||
   * @var \Drupal\Core\Session\AccountInterface
 | 
			
		||||
   */
 | 
			
		||||
  protected $adminUser;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = [
 | 
			
		||||
    'block',
 | 
			
		||||
    'content_moderation',
 | 
			
		||||
    'node',
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
 | 
			
		||||
    $this->drupalPlaceBlock('local_tasks_block');
 | 
			
		||||
 | 
			
		||||
    $this->adminUser = $this->drupalCreateUser([
 | 
			
		||||
      'access administration pages',
 | 
			
		||||
      'access content overview',
 | 
			
		||||
      'view any unpublished content',
 | 
			
		||||
    ]);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the moderated content local task appears.
 | 
			
		||||
   */
 | 
			
		||||
  public function testModeratedContentLocalTask(): void {
 | 
			
		||||
    $this->drupalLogin($this->adminUser);
 | 
			
		||||
 | 
			
		||||
    // Verify the moderated content tab exists.
 | 
			
		||||
    $this->drupalGet('admin/content');
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
    $this->assertSession()->linkExists('Moderated content');
 | 
			
		||||
 | 
			
		||||
    // Uninstall the node module which should also remove the tab.
 | 
			
		||||
    $this->container->get('module_installer')->uninstall(['node']);
 | 
			
		||||
 | 
			
		||||
    // Verify the moderated content local task does not exist without the node
 | 
			
		||||
    // module installed.
 | 
			
		||||
    $this->drupalGet('admin/content');
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(403);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,181 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\content_moderation\Functional;
 | 
			
		||||
 | 
			
		||||
use Drupal\language\Entity\ConfigurableLanguage;
 | 
			
		||||
use Drupal\node\Entity\Node;
 | 
			
		||||
use Drupal\Tests\BrowserTestBase;
 | 
			
		||||
use Drupal\Tests\content_moderation\Traits\ContentModerationTestTrait;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests moderated content administration page functionality.
 | 
			
		||||
 *
 | 
			
		||||
 * @group content_moderation
 | 
			
		||||
 */
 | 
			
		||||
class ModeratedContentViewTest extends BrowserTestBase {
 | 
			
		||||
 | 
			
		||||
  use ContentModerationTestTrait;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * A user with permission to bypass access content.
 | 
			
		||||
   *
 | 
			
		||||
   * @var \Drupal\Core\Session\AccountInterface
 | 
			
		||||
   */
 | 
			
		||||
  protected $adminUser;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = [
 | 
			
		||||
    'content_moderation',
 | 
			
		||||
    'node',
 | 
			
		||||
    'views',
 | 
			
		||||
    'language',
 | 
			
		||||
    'content_translation',
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
 | 
			
		||||
    $this->drupalCreateContentType(['type' => 'page', 'name' => 'Basic page'])->save();
 | 
			
		||||
    $this->drupalCreateContentType(['type' => 'article', 'name' => 'Article'])->save();
 | 
			
		||||
    $this->drupalCreateContentType(['type' => 'unmoderated_type', 'name' => 'Unmoderated type'])->save();
 | 
			
		||||
 | 
			
		||||
    $workflow = $this->createEditorialWorkflow();
 | 
			
		||||
    $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'page');
 | 
			
		||||
    $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'article');
 | 
			
		||||
    $workflow->save();
 | 
			
		||||
 | 
			
		||||
    $this->adminUser = $this->drupalCreateUser([
 | 
			
		||||
      'access administration pages',
 | 
			
		||||
      'view any unpublished content',
 | 
			
		||||
      'administer nodes',
 | 
			
		||||
      'bypass node access',
 | 
			
		||||
    ]);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the moderated content page.
 | 
			
		||||
   */
 | 
			
		||||
  public function testModeratedContentPage(): void {
 | 
			
		||||
    $assert_session = $this->assertSession();
 | 
			
		||||
    $this->drupalLogin($this->adminUser);
 | 
			
		||||
 | 
			
		||||
    // Use an explicit changed time to ensure the expected order in the content
 | 
			
		||||
    // admin listing. We want these to appear in the table in the same order as
 | 
			
		||||
    // they appear in the following code, and the 'moderated_content' view has a
 | 
			
		||||
    // table style configuration with a default sort on the 'changed' field
 | 
			
		||||
    // descending.
 | 
			
		||||
    $time = \Drupal::time()->getRequestTime();
 | 
			
		||||
    $excluded_nodes['published_page'] = $this->drupalCreateNode(['type' => 'page', 'changed' => $time--, 'moderation_state' => 'published']);
 | 
			
		||||
    $excluded_nodes['published_article'] = $this->drupalCreateNode(['type' => 'article', 'changed' => $time--, 'moderation_state' => 'published']);
 | 
			
		||||
 | 
			
		||||
    $excluded_nodes['unmoderated_type'] = $this->drupalCreateNode(['type' => 'unmoderated_type', 'changed' => $time--]);
 | 
			
		||||
    $excluded_nodes['unmoderated_type']->setNewRevision(TRUE);
 | 
			
		||||
    $excluded_nodes['unmoderated_type']->isDefaultRevision(FALSE);
 | 
			
		||||
    $excluded_nodes['unmoderated_type']->changed->value = $time--;
 | 
			
		||||
    $excluded_nodes['unmoderated_type']->save();
 | 
			
		||||
 | 
			
		||||
    $nodes['published_then_draft_article'] = $this->drupalCreateNode(['type' => 'article', 'changed' => $time--, 'moderation_state' => 'published', 'title' => 'first article - published']);
 | 
			
		||||
    $nodes['published_then_draft_article']->setNewRevision(TRUE);
 | 
			
		||||
    $nodes['published_then_draft_article']->setTitle('first article - draft');
 | 
			
		||||
    $nodes['published_then_draft_article']->moderation_state->value = 'draft';
 | 
			
		||||
    $nodes['published_then_draft_article']->changed->value = $time--;
 | 
			
		||||
    $nodes['published_then_draft_article']->save();
 | 
			
		||||
 | 
			
		||||
    $nodes['published_then_archived_article'] = $this->drupalCreateNode(['type' => 'article', 'changed' => $time--, 'moderation_state' => 'published']);
 | 
			
		||||
    $nodes['published_then_archived_article']->setNewRevision(TRUE);
 | 
			
		||||
    $nodes['published_then_archived_article']->moderation_state->value = 'archived';
 | 
			
		||||
    $nodes['published_then_archived_article']->changed->value = $time--;
 | 
			
		||||
    $nodes['published_then_archived_article']->save();
 | 
			
		||||
 | 
			
		||||
    $nodes['draft_article'] = $this->drupalCreateNode(['type' => 'article', 'changed' => $time--, 'moderation_state' => 'draft']);
 | 
			
		||||
    $nodes['draft_page_1'] = $this->drupalCreateNode(['type' => 'page', 'changed' => $time--, 'moderation_state' => 'draft']);
 | 
			
		||||
    $nodes['draft_page_2'] = $this->drupalCreateNode(['type' => 'page', 'changed' => $time, 'moderation_state' => 'draft']);
 | 
			
		||||
 | 
			
		||||
    // Verify view, edit, and delete links for any content.
 | 
			
		||||
    $this->drupalGet('admin/content/moderated');
 | 
			
		||||
    $assert_session->statusCodeEquals(200);
 | 
			
		||||
 | 
			
		||||
    // Check that nodes with pending revisions appear in the view.
 | 
			
		||||
    $node_type_labels = $this->xpath('//td[contains(@class, "views-field-type")]');
 | 
			
		||||
    $delta = 0;
 | 
			
		||||
    foreach ($nodes as $node) {
 | 
			
		||||
      $assert_session->linkByHrefExists('node/' . $node->id());
 | 
			
		||||
      $assert_session->linkByHrefExists('node/' . $node->id() . '/edit');
 | 
			
		||||
      $assert_session->linkByHrefExists('node/' . $node->id() . '/delete');
 | 
			
		||||
      // Verify that we can see the content type label.
 | 
			
		||||
      $this->assertEquals($node->type->entity->label(), trim($node_type_labels[$delta]->getText()));
 | 
			
		||||
      $delta++;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Check that nodes that are not moderated or do not have a pending revision
 | 
			
		||||
    // do not appear in the view.
 | 
			
		||||
    foreach ($excluded_nodes as $node) {
 | 
			
		||||
      $assert_session->linkByHrefNotExists('node/' . $node->id());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Check that the latest revision is displayed.
 | 
			
		||||
    $assert_session->pageTextContains('first article - draft');
 | 
			
		||||
    $assert_session->pageTextNotContains('first article - published');
 | 
			
		||||
 | 
			
		||||
    // Verify filtering by moderation state.
 | 
			
		||||
    $this->drupalGet('admin/content/moderated', ['query' => ['moderation_state' => 'editorial-draft']]);
 | 
			
		||||
 | 
			
		||||
    $assert_session->linkByHrefExists('node/' . $nodes['published_then_draft_article']->id() . '/edit');
 | 
			
		||||
    $assert_session->linkByHrefExists('node/' . $nodes['draft_article']->id() . '/edit');
 | 
			
		||||
    $assert_session->linkByHrefExists('node/' . $nodes['draft_page_1']->id() . '/edit');
 | 
			
		||||
    $assert_session->linkByHrefExists('node/' . $nodes['draft_page_1']->id() . '/edit');
 | 
			
		||||
    $assert_session->linkByHrefNotExists('node/' . $nodes['published_then_archived_article']->id() . '/edit');
 | 
			
		||||
 | 
			
		||||
    // Verify filtering by moderation state and content type.
 | 
			
		||||
    $this->drupalGet('admin/content/moderated', ['query' => ['moderation_state' => 'editorial-draft', 'type' => 'page']]);
 | 
			
		||||
 | 
			
		||||
    $assert_session->linkByHrefExists('node/' . $nodes['draft_page_1']->id() . '/edit');
 | 
			
		||||
    $assert_session->linkByHrefExists('node/' . $nodes['draft_page_2']->id() . '/edit');
 | 
			
		||||
    $assert_session->linkByHrefNotExists('node/' . $nodes['published_then_draft_article']->id() . '/edit');
 | 
			
		||||
    $assert_session->linkByHrefNotExists('node/' . $nodes['published_then_archived_article']->id() . '/edit');
 | 
			
		||||
    $assert_session->linkByHrefNotExists('node/' . $nodes['draft_article']->id() . '/edit');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the moderated content page with multilingual content.
 | 
			
		||||
   */
 | 
			
		||||
  public function testModeratedContentPageMultilingual(): void {
 | 
			
		||||
    ConfigurableLanguage::createFromLangcode('fr')->save();
 | 
			
		||||
 | 
			
		||||
    $node = $this->drupalCreateNode([
 | 
			
		||||
      'type' => 'article',
 | 
			
		||||
      'moderation_state' => 'published',
 | 
			
		||||
      'title' => 'en article published',
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    $node->title = 'en draft revision';
 | 
			
		||||
    $node->moderation_state = 'draft';
 | 
			
		||||
    $node->save();
 | 
			
		||||
 | 
			
		||||
    $translation = Node::load($node->id())->addTranslation('fr');
 | 
			
		||||
    $translation->title = 'fr draft revision';
 | 
			
		||||
    $translation->moderation_state = 'draft';
 | 
			
		||||
    $translation->save();
 | 
			
		||||
 | 
			
		||||
    $this->drupalLogin($this->adminUser);
 | 
			
		||||
 | 
			
		||||
    // The moderated content view should show both the pending en draft revision
 | 
			
		||||
    // and the pending fr draft revision.
 | 
			
		||||
    $this->drupalGet('admin/content/moderated');
 | 
			
		||||
    $this->assertSession()->linkExists('fr draft revision');
 | 
			
		||||
    $this->assertSession()->linkExists('en draft revision');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,142 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\content_moderation\Functional;
 | 
			
		||||
 | 
			
		||||
use Drupal\node\Entity\Node;
 | 
			
		||||
use Drupal\Tests\BrowserTestBase;
 | 
			
		||||
use Drupal\Tests\content_moderation\Traits\ContentModerationTestTrait;
 | 
			
		||||
use Drupal\Tests\node\Traits\ContentTypeCreationTrait;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Test the content moderation actions.
 | 
			
		||||
 *
 | 
			
		||||
 * @group content_moderation
 | 
			
		||||
 */
 | 
			
		||||
class ModerationActionsTest extends BrowserTestBase {
 | 
			
		||||
 | 
			
		||||
  use ContentTypeCreationTrait;
 | 
			
		||||
  use ContentModerationTestTrait;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = [
 | 
			
		||||
    'content_moderation',
 | 
			
		||||
    'node',
 | 
			
		||||
    'views',
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
 | 
			
		||||
    $moderated_bundle = $this->createContentType(['type' => 'moderated_bundle']);
 | 
			
		||||
    $moderated_bundle->save();
 | 
			
		||||
    $standard_bundle = $this->createContentType(['type' => 'standard_bundle']);
 | 
			
		||||
    $standard_bundle->save();
 | 
			
		||||
 | 
			
		||||
    $workflow = $this->createEditorialWorkflow();
 | 
			
		||||
    $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'moderated_bundle');
 | 
			
		||||
    $workflow->save();
 | 
			
		||||
 | 
			
		||||
    $admin = $this->drupalCreateUser([
 | 
			
		||||
      'access content overview',
 | 
			
		||||
      'administer nodes',
 | 
			
		||||
      'bypass node access',
 | 
			
		||||
    ]);
 | 
			
		||||
    $this->drupalLogin($admin);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the node status actions report moderation status to users correctly.
 | 
			
		||||
   *
 | 
			
		||||
   * @dataProvider nodeStatusActionsTestCases
 | 
			
		||||
   */
 | 
			
		||||
  public function testNodeStatusActions($action, $bundle, $warning_appears, $starting_status, $final_status): void {
 | 
			
		||||
    // Create and run an action on a node.
 | 
			
		||||
    $node = Node::create([
 | 
			
		||||
      'type' => $bundle,
 | 
			
		||||
      'title' => $this->randomString(),
 | 
			
		||||
      'status' => $starting_status,
 | 
			
		||||
    ]);
 | 
			
		||||
    if ($bundle == 'moderated_bundle') {
 | 
			
		||||
      $node->moderation_state->value = $starting_status ? 'published' : 'draft';
 | 
			
		||||
    }
 | 
			
		||||
    $node->save();
 | 
			
		||||
 | 
			
		||||
    $this->drupalGet('admin/content');
 | 
			
		||||
    $this->submitForm([
 | 
			
		||||
      'node_bulk_form[0]' => TRUE,
 | 
			
		||||
      'action' => $action,
 | 
			
		||||
    ], 'Apply to selected items');
 | 
			
		||||
 | 
			
		||||
    if ($warning_appears) {
 | 
			
		||||
      if ($action == 'node_publish_action') {
 | 
			
		||||
        $this->assertSession()->statusMessageContains(node_get_type_label($node) . ' content items were skipped as they are under moderation and may not be directly published.', 'warning');
 | 
			
		||||
      }
 | 
			
		||||
      else {
 | 
			
		||||
        $this->assertSession()->statusMessageContains(node_get_type_label($node) . ' content items were skipped as they are under moderation and may not be directly unpublished.', 'warning');
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
      $this->assertSession()->statusMessageNotExists('warning');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Ensure after the action has run, the node matches the expected status.
 | 
			
		||||
    $node = Node::load($node->id());
 | 
			
		||||
    $this->assertEquals($node->isPublished(), $final_status);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Test cases for ::testNodeStatusActions.
 | 
			
		||||
   *
 | 
			
		||||
   * @return array
 | 
			
		||||
   *   An array of test cases.
 | 
			
		||||
   */
 | 
			
		||||
  public static function nodeStatusActionsTestCases() {
 | 
			
		||||
    return [
 | 
			
		||||
      'Moderated bundle shows warning (publish action)' => [
 | 
			
		||||
        'node_publish_action',
 | 
			
		||||
        'moderated_bundle',
 | 
			
		||||
        TRUE,
 | 
			
		||||
        // If the node starts out unpublished, the action should not work.
 | 
			
		||||
        FALSE,
 | 
			
		||||
        FALSE,
 | 
			
		||||
      ],
 | 
			
		||||
      'Moderated bundle shows warning (unpublish action)' => [
 | 
			
		||||
        'node_unpublish_action',
 | 
			
		||||
        'moderated_bundle',
 | 
			
		||||
        TRUE,
 | 
			
		||||
        // If the node starts out published, the action should not work.
 | 
			
		||||
        TRUE,
 | 
			
		||||
        TRUE,
 | 
			
		||||
      ],
 | 
			
		||||
      'Normal bundle works (publish action)' => [
 | 
			
		||||
        'node_publish_action',
 | 
			
		||||
        'standard_bundle',
 | 
			
		||||
        FALSE,
 | 
			
		||||
        // If the node starts out unpublished, the action should work.
 | 
			
		||||
        FALSE,
 | 
			
		||||
        TRUE,
 | 
			
		||||
      ],
 | 
			
		||||
      'Normal bundle works (unpublish action)' => [
 | 
			
		||||
        'node_unpublish_action',
 | 
			
		||||
        'standard_bundle',
 | 
			
		||||
        FALSE,
 | 
			
		||||
        // If the node starts out published, the action should work.
 | 
			
		||||
        TRUE,
 | 
			
		||||
        FALSE,
 | 
			
		||||
      ],
 | 
			
		||||
    ];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,114 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\content_moderation\Functional;
 | 
			
		||||
 | 
			
		||||
use Drupal\Tests\BrowserTestBase;
 | 
			
		||||
use Drupal\Tests\content_moderation\Traits\ContentModerationTestTrait;
 | 
			
		||||
use Drupal\Tests\content_translation\Traits\ContentTranslationTestTrait;
 | 
			
		||||
use Drupal\user\Entity\Role;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Test content_moderation functionality with content_translation.
 | 
			
		||||
 *
 | 
			
		||||
 * @group content_moderation
 | 
			
		||||
 */
 | 
			
		||||
class ModerationContentTranslationTest extends BrowserTestBase {
 | 
			
		||||
 | 
			
		||||
  use ContentModerationTestTrait;
 | 
			
		||||
  use ContentTranslationTestTrait;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * A user with permission to bypass access content.
 | 
			
		||||
   *
 | 
			
		||||
   * @var \Drupal\Core\Session\AccountInterface
 | 
			
		||||
   */
 | 
			
		||||
  protected $adminUser;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = [
 | 
			
		||||
    'node',
 | 
			
		||||
    'locale',
 | 
			
		||||
    'content_translation',
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
    $this->adminUser = $this->drupalCreateUser([
 | 
			
		||||
      'bypass node access',
 | 
			
		||||
      'create content translations',
 | 
			
		||||
      'translate any entity',
 | 
			
		||||
    ]);
 | 
			
		||||
    $this->drupalLogin($this->adminUser);
 | 
			
		||||
    // Create an Article content type.
 | 
			
		||||
    $this->drupalCreateContentType(['type' => 'article', 'name' => 'Article'])->save();
 | 
			
		||||
    static::createLanguageFromLangcode('fr');
 | 
			
		||||
    // Enable content translation on articles.
 | 
			
		||||
    $this->enableContentTranslation('node', 'article');
 | 
			
		||||
    // Adding languages requires a container rebuild in the test running
 | 
			
		||||
    // environment so that multilingual services are used.
 | 
			
		||||
    $this->rebuildContainer();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests existing translations being edited after enabling content moderation.
 | 
			
		||||
   */
 | 
			
		||||
  public function testModerationWithExistingContent(): void {
 | 
			
		||||
    // Create a published article in English.
 | 
			
		||||
    $edit = [
 | 
			
		||||
      'title[0][value]' => 'Published English node',
 | 
			
		||||
      'langcode[0][value]' => 'en',
 | 
			
		||||
    ];
 | 
			
		||||
    $this->drupalGet('node/add/article');
 | 
			
		||||
    $this->submitForm($edit, 'Save');
 | 
			
		||||
    $this->assertSession()->pageTextContains('Article Published English node has been created.');
 | 
			
		||||
    $english_node = $this->drupalGetNodeByTitle('Published English node');
 | 
			
		||||
 | 
			
		||||
    // Add a French translation.
 | 
			
		||||
    $this->drupalGet('node/' . $english_node->id() . '/translations');
 | 
			
		||||
    $this->clickLink('Add');
 | 
			
		||||
    $edit = [
 | 
			
		||||
      'title[0][value]' => 'Published French node',
 | 
			
		||||
    ];
 | 
			
		||||
    $this->submitForm($edit, 'Save (this translation)');
 | 
			
		||||
    $this->assertSession()->pageTextContains('Article Published French node has been updated.');
 | 
			
		||||
 | 
			
		||||
    // Install content moderation and enable moderation on Article node type.
 | 
			
		||||
    \Drupal::service('module_installer')->install(['content_moderation']);
 | 
			
		||||
    $workflow = $this->createEditorialWorkflow();
 | 
			
		||||
    $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'article');
 | 
			
		||||
    $workflow->save();
 | 
			
		||||
    $this->grantPermissions(Role::load(Role::AUTHENTICATED_ID), ['use editorial transition publish']);
 | 
			
		||||
 | 
			
		||||
    // Edit the English node.
 | 
			
		||||
    $this->drupalGet('node/' . $english_node->id() . '/edit');
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
    $edit = [
 | 
			
		||||
      'title[0][value]' => 'Published English new node',
 | 
			
		||||
    ];
 | 
			
		||||
    $this->submitForm($edit, 'Save');
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
    $this->assertSession()->pageTextContains('Article Published English new node has been updated.');
 | 
			
		||||
    // Edit the French translation.
 | 
			
		||||
    $this->drupalGet('fr/node/' . $english_node->id() . '/edit');
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
    $edit = [
 | 
			
		||||
      'title[0][value]' => 'Published French new node',
 | 
			
		||||
    ];
 | 
			
		||||
    $this->submitForm($edit, 'Save (this translation)');
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
    $this->assertSession()->pageTextContains('Article Published French new node has been updated.');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,575 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\content_moderation\Functional;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Entity\Entity\EntityFormDisplay;
 | 
			
		||||
use Drupal\Core\Url;
 | 
			
		||||
use Drupal\Tests\content_translation\Traits\ContentTranslationTestTrait;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests the moderation form, specifically on nodes.
 | 
			
		||||
 *
 | 
			
		||||
 * @group content_moderation
 | 
			
		||||
 */
 | 
			
		||||
class ModerationFormTest extends ModerationStateTestBase {
 | 
			
		||||
 | 
			
		||||
  use ContentTranslationTestTrait;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = [
 | 
			
		||||
    'node',
 | 
			
		||||
    'content_moderation',
 | 
			
		||||
    'locale',
 | 
			
		||||
    'content_translation',
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function getAdministratorPermissions(): array {
 | 
			
		||||
    return array_merge($this->permissions, [
 | 
			
		||||
      'administer entity_test content',
 | 
			
		||||
      'view test entity',
 | 
			
		||||
      'translate any entity',
 | 
			
		||||
      'bypass node access',
 | 
			
		||||
    ]);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
    $this->drupalLogin($this->adminUser);
 | 
			
		||||
    $this->createContentTypeFromUi('Moderated content', 'moderated_content', TRUE);
 | 
			
		||||
    $this->grantUserPermissionToCreateContentOfType($this->adminUser, 'moderated_content');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the moderation form that shows on the latest version page.
 | 
			
		||||
   *
 | 
			
		||||
   * The latest version page only shows if there is a pending revision.
 | 
			
		||||
   *
 | 
			
		||||
   * @see \Drupal\content_moderation\EntityOperations
 | 
			
		||||
   * @see \Drupal\Tests\content_moderation\Functional\ModerationStateBlockTest::testCustomBlockModeration
 | 
			
		||||
   */
 | 
			
		||||
  public function testModerationForm(): void {
 | 
			
		||||
    // Test the states that appear by default when creating a new item of
 | 
			
		||||
    // content.
 | 
			
		||||
    $this->drupalGet('node/add/moderated_content');
 | 
			
		||||
    $this->assertSession()->optionExists('moderation_state[0][state]', 'draft');
 | 
			
		||||
    $this->assertSession()->optionExists('moderation_state[0][state]', 'published');
 | 
			
		||||
    $this->assertSession()->optionNotExists('moderation_state[0][state]', 'archived');
 | 
			
		||||
    // Previewing a new item of content should not change the available states.
 | 
			
		||||
    $this->submitForm([
 | 
			
		||||
      'moderation_state[0][state]' => 'published',
 | 
			
		||||
      'title[0][value]' => 'Some moderated content',
 | 
			
		||||
      'body[0][value]' => 'First version of the content.',
 | 
			
		||||
    ], 'Preview');
 | 
			
		||||
    $this->clickLink('Back to content editing');
 | 
			
		||||
    $this->assertSession()->optionExists('moderation_state[0][state]', 'draft');
 | 
			
		||||
    $this->assertSession()->optionExists('moderation_state[0][state]', 'published');
 | 
			
		||||
    $this->assertSession()->optionNotExists('moderation_state[0][state]', 'archived');
 | 
			
		||||
 | 
			
		||||
    // Create new moderated content in draft.
 | 
			
		||||
    $this->submitForm(['moderation_state[0][state]' => 'draft'], 'Save');
 | 
			
		||||
 | 
			
		||||
    $node = $this->drupalGetNodeByTitle('Some moderated content');
 | 
			
		||||
    $canonical_path = sprintf('node/%d', $node->id());
 | 
			
		||||
    $edit_path = sprintf('node/%d/edit', $node->id());
 | 
			
		||||
    $latest_version_path = sprintf('node/%d/latest', $node->id());
 | 
			
		||||
 | 
			
		||||
    $this->assertTrue($this->adminUser->hasPermission('edit any moderated_content content'));
 | 
			
		||||
 | 
			
		||||
    // The canonical view should have a moderation form, because it is not the
 | 
			
		||||
    // live revision.
 | 
			
		||||
    $this->drupalGet($canonical_path);
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
    $this->assertSession()->fieldExists('edit-new-state');
 | 
			
		||||
 | 
			
		||||
    // The latest version page should not show, because there is no pending
 | 
			
		||||
    // revision.
 | 
			
		||||
    $this->drupalGet($latest_version_path);
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(403);
 | 
			
		||||
 | 
			
		||||
    // Update the draft.
 | 
			
		||||
    $this->drupalGet($edit_path);
 | 
			
		||||
    $this->submitForm([
 | 
			
		||||
      'body[0][value]' => 'Second version of the content.',
 | 
			
		||||
      'moderation_state[0][state]' => 'draft',
 | 
			
		||||
    ], 'Save');
 | 
			
		||||
 | 
			
		||||
    // The canonical view should have a moderation form, because it is not the
 | 
			
		||||
    // live revision.
 | 
			
		||||
    $this->drupalGet($canonical_path);
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
    $this->assertSession()->fieldExists('edit-new-state');
 | 
			
		||||
 | 
			
		||||
    // Preview the draft.
 | 
			
		||||
    $this->drupalGet($edit_path);
 | 
			
		||||
    $this->submitForm([
 | 
			
		||||
      'body[0][value]' => 'Second version of the content.',
 | 
			
		||||
      'moderation_state[0][state]' => 'draft',
 | 
			
		||||
    ], 'Preview');
 | 
			
		||||
 | 
			
		||||
    // The preview view should not have a moderation form.
 | 
			
		||||
    $preview_url = Url::fromRoute('entity.node.preview', [
 | 
			
		||||
      'node_preview' => $node->uuid(),
 | 
			
		||||
      'view_mode_id' => 'full',
 | 
			
		||||
    ]);
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
    $this->assertSession()->addressEquals($preview_url);
 | 
			
		||||
    $this->assertSession()->fieldNotExists('edit-new-state');
 | 
			
		||||
 | 
			
		||||
    // The latest version page should not show, because there is still no
 | 
			
		||||
    // pending revision.
 | 
			
		||||
    $this->drupalGet($latest_version_path);
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(403);
 | 
			
		||||
 | 
			
		||||
    // Publish the draft.
 | 
			
		||||
    $this->drupalGet($edit_path);
 | 
			
		||||
    $this->submitForm([
 | 
			
		||||
      'body[0][value]' => 'Third version of the content.',
 | 
			
		||||
      'moderation_state[0][state]' => 'published',
 | 
			
		||||
    ], 'Save');
 | 
			
		||||
 | 
			
		||||
    // Check widget default value.
 | 
			
		||||
    $this->drupalGet($edit_path);
 | 
			
		||||
    $this->assertSession()->fieldValueEquals('moderation_state[0][state]', 'published');
 | 
			
		||||
 | 
			
		||||
    // Preview the content while selecting the "draft" state and when the user
 | 
			
		||||
    // returns to the edit form, ensure all of the available transitions are
 | 
			
		||||
    // still those available from the "published" source state.
 | 
			
		||||
    $this->submitForm(['moderation_state[0][state]' => 'draft'], 'Preview');
 | 
			
		||||
    $this->clickLink('Back to content editing');
 | 
			
		||||
    $this->assertSession()->optionExists('moderation_state[0][state]', 'draft');
 | 
			
		||||
    $this->assertSession()->optionExists('moderation_state[0][state]', 'published');
 | 
			
		||||
    $this->assertSession()->optionExists('moderation_state[0][state]', 'archived');
 | 
			
		||||
 | 
			
		||||
    // The published view should not have a moderation form, because it is the
 | 
			
		||||
    // live revision.
 | 
			
		||||
    $this->drupalGet($canonical_path);
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
    $this->assertSession()->fieldNotExists('edit-new-state');
 | 
			
		||||
 | 
			
		||||
    // The latest version page should not show, because there is still no
 | 
			
		||||
    // pending revision.
 | 
			
		||||
    $this->drupalGet($latest_version_path);
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(403);
 | 
			
		||||
 | 
			
		||||
    // Make a pending revision.
 | 
			
		||||
    $this->drupalGet($edit_path);
 | 
			
		||||
    $this->submitForm([
 | 
			
		||||
      'body[0][value]' => 'Fourth version of the content.',
 | 
			
		||||
      'moderation_state[0][state]' => 'draft',
 | 
			
		||||
    ], 'Save');
 | 
			
		||||
 | 
			
		||||
    // The published view should not have a moderation form, because it is the
 | 
			
		||||
    // live revision.
 | 
			
		||||
    $this->drupalGet($canonical_path);
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
    $this->assertSession()->fieldNotExists('edit-new-state');
 | 
			
		||||
 | 
			
		||||
    // The latest version page should show the moderation form and have "Draft"
 | 
			
		||||
    // status, because the pending revision is in "Draft".
 | 
			
		||||
    $this->drupalGet($latest_version_path);
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
    $this->assertSession()->fieldExists('edit-new-state');
 | 
			
		||||
    $this->assertSession()->pageTextContains('Draft');
 | 
			
		||||
 | 
			
		||||
    // Submit the moderation form to change status to published.
 | 
			
		||||
    $this->drupalGet($latest_version_path);
 | 
			
		||||
    $this->submitForm(['new_state' => 'published'], 'Apply');
 | 
			
		||||
 | 
			
		||||
    // The latest version page should not show, because there is no
 | 
			
		||||
    // pending revision.
 | 
			
		||||
    $this->drupalGet($latest_version_path);
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(403);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests moderation non-bundle entity type.
 | 
			
		||||
   */
 | 
			
		||||
  public function testNonBundleModerationForm(): void {
 | 
			
		||||
    $this->adminUser = $this->drupalCreateUser($this->getAdministratorPermissions());
 | 
			
		||||
    $this->drupalLogin($this->adminUser);
 | 
			
		||||
    $this->workflow->getTypePlugin()->addEntityTypeAndBundle('entity_test_mulrevpub', 'entity_test_mulrevpub');
 | 
			
		||||
    $this->workflow->save();
 | 
			
		||||
 | 
			
		||||
    // Create new moderated content in draft.
 | 
			
		||||
    $this->drupalGet('entity_test_mulrevpub/add');
 | 
			
		||||
    $this->submitForm(['moderation_state[0][state]' => 'draft'], 'Save');
 | 
			
		||||
 | 
			
		||||
    // The latest version page should not show, because there is no pending
 | 
			
		||||
    // revision.
 | 
			
		||||
    $this->drupalGet('/entity_test_mulrevpub/manage/1/latest');
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(403);
 | 
			
		||||
 | 
			
		||||
    // Update the draft.
 | 
			
		||||
    $this->drupalGet('entity_test_mulrevpub/manage/1/edit');
 | 
			
		||||
    $this->submitForm(['moderation_state[0][state]' => 'draft'], 'Save');
 | 
			
		||||
 | 
			
		||||
    // The latest version page should not show, because there is still no
 | 
			
		||||
    // pending revision.
 | 
			
		||||
    $this->drupalGet('/entity_test_mulrevpub/manage/1/latest');
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(403);
 | 
			
		||||
 | 
			
		||||
    // Publish the draft.
 | 
			
		||||
    $this->drupalGet('entity_test_mulrevpub/manage/1/edit');
 | 
			
		||||
    $this->submitForm(['moderation_state[0][state]' => 'published'], 'Save');
 | 
			
		||||
 | 
			
		||||
    // The published view should not have a moderation form, because it is the
 | 
			
		||||
    // default revision.
 | 
			
		||||
    $this->drupalGet('entity_test_mulrevpub/manage/1');
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
    $this->assertSession()->pageTextNotContains('Status');
 | 
			
		||||
 | 
			
		||||
    // The latest version page should not show, because there is still no
 | 
			
		||||
    // pending revision.
 | 
			
		||||
    $this->drupalGet('entity_test_mulrevpub/manage/1/latest');
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(403);
 | 
			
		||||
 | 
			
		||||
    // Make a pending revision.
 | 
			
		||||
    $this->drupalGet('entity_test_mulrevpub/manage/1/edit');
 | 
			
		||||
    $this->submitForm(['moderation_state[0][state]' => 'draft'], 'Save');
 | 
			
		||||
 | 
			
		||||
    // The published view should not have a moderation form, because it is the
 | 
			
		||||
    // default revision.
 | 
			
		||||
    $this->drupalGet('entity_test_mulrevpub/manage/1');
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
    $this->assertSession()->pageTextNotContains('Status');
 | 
			
		||||
 | 
			
		||||
    // The latest version page should show the moderation form and have "Draft"
 | 
			
		||||
    // status, because the pending revision is in "Draft".
 | 
			
		||||
    $this->drupalGet('entity_test_mulrevpub/manage/1/latest');
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
    $this->assertSession()->pageTextContains('Moderation state');
 | 
			
		||||
    $this->assertSession()->pageTextContains('Draft');
 | 
			
		||||
 | 
			
		||||
    // Submit the moderation form to change status to published.
 | 
			
		||||
    $this->drupalGet('entity_test_mulrevpub/manage/1/latest');
 | 
			
		||||
    $this->submitForm(['new_state' => 'published'], 'Apply');
 | 
			
		||||
 | 
			
		||||
    // The latest version page should not show, because there is no
 | 
			
		||||
    // pending revision.
 | 
			
		||||
    $this->drupalGet('entity_test_mulrevpub/manage/1/latest');
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(403);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the revision author is updated when the moderation form is used.
 | 
			
		||||
   */
 | 
			
		||||
  public function testModerationFormSetsRevisionAuthor(): void {
 | 
			
		||||
    // Create new moderated content in published.
 | 
			
		||||
    $node = $this->createNode(['type' => 'moderated_content', 'moderation_state' => 'published']);
 | 
			
		||||
    // Make a pending revision.
 | 
			
		||||
    $node->title = $this->randomMachineName();
 | 
			
		||||
    $node->moderation_state->value = 'draft';
 | 
			
		||||
    $node->setRevisionCreationTime(12345);
 | 
			
		||||
    $node->save();
 | 
			
		||||
 | 
			
		||||
    $another_user = $this->drupalCreateUser($this->permissions);
 | 
			
		||||
    $this->grantUserPermissionToCreateContentOfType($another_user, 'moderated_content');
 | 
			
		||||
    $this->drupalLogin($another_user);
 | 
			
		||||
    $this->drupalGet(sprintf('node/%d/latest', $node->id()));
 | 
			
		||||
    $this->submitForm(['new_state' => 'published'], 'Apply');
 | 
			
		||||
 | 
			
		||||
    $this->drupalGet(sprintf('node/%d/revisions', $node->id()));
 | 
			
		||||
    $this->assertSession()->pageTextContains('by ' . $another_user->getAccountName());
 | 
			
		||||
 | 
			
		||||
    // Verify the revision creation time has been updated.
 | 
			
		||||
    $node = $node->load($node->id());
 | 
			
		||||
    $this->assertGreaterThan(12345, $node->getRevisionCreationTime());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests translated and moderated nodes.
 | 
			
		||||
   */
 | 
			
		||||
  public function testContentTranslationNodeForm(): void {
 | 
			
		||||
    $this->adminUser = $this->drupalCreateUser($this->getAdministratorPermissions());
 | 
			
		||||
    $this->drupalLogin($this->adminUser);
 | 
			
		||||
 | 
			
		||||
    // Add French language.
 | 
			
		||||
    static::createLanguageFromLangcode('fr');
 | 
			
		||||
 | 
			
		||||
    // Enable content translation on moderated_content.
 | 
			
		||||
    $this->enableContentTranslation('node', 'moderated_content');
 | 
			
		||||
 | 
			
		||||
    // Adding languages requires a container rebuild in the test running
 | 
			
		||||
    // environment so that multilingual services are used.
 | 
			
		||||
    $this->rebuildContainer();
 | 
			
		||||
 | 
			
		||||
    // Create new moderated content in draft (revision 1).
 | 
			
		||||
    $this->drupalGet('node/add/moderated_content');
 | 
			
		||||
    $this->submitForm([
 | 
			
		||||
      'title[0][value]' => 'Some moderated content',
 | 
			
		||||
      'body[0][value]' => 'First version of the content.',
 | 
			
		||||
      'moderation_state[0][state]' => 'draft',
 | 
			
		||||
    ], 'Save');
 | 
			
		||||
    $this->assertSession()->elementExists('xpath', '//ul[@class="entity-moderation-form"]');
 | 
			
		||||
 | 
			
		||||
    $node = $this->drupalGetNodeByTitle('Some moderated content');
 | 
			
		||||
    $this->assertNotEmpty($node->language(), 'en');
 | 
			
		||||
    $edit_path = sprintf('node/%d/edit', $node->id());
 | 
			
		||||
    $translate_path = sprintf('node/%d/translations/add/en/fr', $node->id());
 | 
			
		||||
    $latest_version_path = sprintf('node/%d/latest', $node->id());
 | 
			
		||||
    $french = \Drupal::languageManager()->getLanguage('fr');
 | 
			
		||||
 | 
			
		||||
    $this->drupalGet($latest_version_path);
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(403);
 | 
			
		||||
    $this->assertSession()->elementNotExists('xpath', '//ul[@class="entity-moderation-form"]');
 | 
			
		||||
 | 
			
		||||
    // Add french translation (revision 2).
 | 
			
		||||
    $this->drupalGet($translate_path);
 | 
			
		||||
    $this->assertSession()->optionExists('moderation_state[0][state]', 'draft');
 | 
			
		||||
    $this->assertSession()->optionExists('moderation_state[0][state]', 'published');
 | 
			
		||||
    $this->assertSession()->optionNotExists('moderation_state[0][state]', 'archived');
 | 
			
		||||
    $this->submitForm([
 | 
			
		||||
      'body[0][value]' => 'Second version of the content.',
 | 
			
		||||
      'moderation_state[0][state]' => 'published',
 | 
			
		||||
    ], 'Save (this translation)');
 | 
			
		||||
 | 
			
		||||
    $this->drupalGet($latest_version_path, ['language' => $french]);
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(403);
 | 
			
		||||
    $this->assertSession()->elementNotExists('xpath', '//ul[@class="entity-moderation-form"]');
 | 
			
		||||
 | 
			
		||||
    // Add french pending revision (revision 3).
 | 
			
		||||
    $this->drupalGet($edit_path, ['language' => $french]);
 | 
			
		||||
    $this->assertSession()->optionExists('moderation_state[0][state]', 'draft');
 | 
			
		||||
    $this->assertSession()->optionExists('moderation_state[0][state]', 'published');
 | 
			
		||||
    $this->assertSession()->optionExists('moderation_state[0][state]', 'archived');
 | 
			
		||||
 | 
			
		||||
    // Preview the content while selecting the "draft" state and when the user
 | 
			
		||||
    // returns to the edit form, ensure all of the available transitions are
 | 
			
		||||
    // still those available from the "published" source state.
 | 
			
		||||
    $this->submitForm(['moderation_state[0][state]' => 'draft'], 'Preview');
 | 
			
		||||
    $this->clickLink('Back to content editing');
 | 
			
		||||
    $this->assertSession()->optionExists('moderation_state[0][state]', 'draft');
 | 
			
		||||
    $this->assertSession()->optionExists('moderation_state[0][state]', 'published');
 | 
			
		||||
    $this->assertSession()->optionExists('moderation_state[0][state]', 'archived');
 | 
			
		||||
 | 
			
		||||
    $this->submitForm([
 | 
			
		||||
      'body[0][value]' => 'Third version of the content.',
 | 
			
		||||
      'moderation_state[0][state]' => 'draft',
 | 
			
		||||
    ], 'Save (this translation)');
 | 
			
		||||
 | 
			
		||||
    $this->drupalGet($latest_version_path, ['language' => $french]);
 | 
			
		||||
    $this->assertSession()->elementExists('xpath', '//ul[@class="entity-moderation-form"]');
 | 
			
		||||
 | 
			
		||||
    $this->drupalGet($edit_path);
 | 
			
		||||
    $this->clickLink('Delete');
 | 
			
		||||
    $this->assertSession()->buttonExists('Delete');
 | 
			
		||||
 | 
			
		||||
    $this->drupalGet($latest_version_path);
 | 
			
		||||
    $this->assertSession()->elementNotExists('xpath', '//ul[@class="entity-moderation-form"]');
 | 
			
		||||
 | 
			
		||||
    // Publish the french pending revision (revision 4).
 | 
			
		||||
    $this->drupalGet($edit_path, ['language' => $french]);
 | 
			
		||||
    $this->assertSession()->optionExists('moderation_state[0][state]', 'draft');
 | 
			
		||||
    $this->assertSession()->optionExists('moderation_state[0][state]', 'published');
 | 
			
		||||
    $this->assertSession()->optionNotExists('moderation_state[0][state]', 'archived');
 | 
			
		||||
    $this->submitForm([
 | 
			
		||||
      'body[0][value]' => 'Fifth version of the content.',
 | 
			
		||||
      'moderation_state[0][state]' => 'published',
 | 
			
		||||
    ], 'Save (this translation)');
 | 
			
		||||
 | 
			
		||||
    $this->drupalGet($latest_version_path, ['language' => $french]);
 | 
			
		||||
    $this->assertSession()->elementNotExists('xpath', '//ul[@class="entity-moderation-form"]');
 | 
			
		||||
 | 
			
		||||
    // Publish the English pending revision (revision 5).
 | 
			
		||||
    $this->drupalGet($edit_path);
 | 
			
		||||
    $this->assertSession()->optionExists('moderation_state[0][state]', 'draft');
 | 
			
		||||
    $this->assertSession()->optionExists('moderation_state[0][state]', 'published');
 | 
			
		||||
    $this->assertSession()->optionNotExists('moderation_state[0][state]', 'archived');
 | 
			
		||||
    $this->submitForm([
 | 
			
		||||
      'body[0][value]' => 'Sixth version of the content.',
 | 
			
		||||
      'moderation_state[0][state]' => 'published',
 | 
			
		||||
    ], 'Save (this translation)');
 | 
			
		||||
 | 
			
		||||
    $this->drupalGet($latest_version_path);
 | 
			
		||||
    $this->assertSession()->elementNotExists('xpath', '//ul[@class="entity-moderation-form"]');
 | 
			
		||||
 | 
			
		||||
    // Make sure we are allowed to create a pending French revision.
 | 
			
		||||
    $this->drupalGet($edit_path, ['language' => $french]);
 | 
			
		||||
    $this->assertSession()->optionExists('moderation_state[0][state]', 'draft');
 | 
			
		||||
    $this->assertSession()->optionExists('moderation_state[0][state]', 'published');
 | 
			
		||||
    $this->assertSession()->optionExists('moderation_state[0][state]', 'archived');
 | 
			
		||||
 | 
			
		||||
    // Add an English pending revision (revision 6).
 | 
			
		||||
    $this->drupalGet($edit_path);
 | 
			
		||||
    $this->assertSession()->optionExists('moderation_state[0][state]', 'draft');
 | 
			
		||||
    $this->assertSession()->optionExists('moderation_state[0][state]', 'published');
 | 
			
		||||
    $this->assertSession()->optionExists('moderation_state[0][state]', 'archived');
 | 
			
		||||
    $this->submitForm([
 | 
			
		||||
      'body[0][value]' => 'Seventh version of the content.',
 | 
			
		||||
      'moderation_state[0][state]' => 'draft',
 | 
			
		||||
    ], 'Save (this translation)');
 | 
			
		||||
 | 
			
		||||
    $this->drupalGet($latest_version_path);
 | 
			
		||||
    $this->assertSession()->elementExists('xpath', '//ul[@class="entity-moderation-form"]');
 | 
			
		||||
    $this->drupalGet($latest_version_path, ['language' => $french]);
 | 
			
		||||
    $this->assertSession()->elementNotExists('xpath', '//ul[@class="entity-moderation-form"]');
 | 
			
		||||
 | 
			
		||||
    // Publish the English pending revision (revision 7)
 | 
			
		||||
    $this->drupalGet($edit_path);
 | 
			
		||||
    $this->assertSession()->optionExists('moderation_state[0][state]', 'draft');
 | 
			
		||||
    $this->assertSession()->optionExists('moderation_state[0][state]', 'published');
 | 
			
		||||
    $this->assertSession()->optionNotExists('moderation_state[0][state]', 'archived');
 | 
			
		||||
    $this->submitForm([
 | 
			
		||||
      'body[0][value]' => 'Eighth version of the content.',
 | 
			
		||||
      'moderation_state[0][state]' => 'published',
 | 
			
		||||
    ], 'Save (this translation)');
 | 
			
		||||
 | 
			
		||||
    $this->drupalGet($latest_version_path);
 | 
			
		||||
    $this->assertSession()->elementNotExists('xpath', '//ul[@class="entity-moderation-form"]');
 | 
			
		||||
 | 
			
		||||
    // Make sure we are allowed to create a pending French revision.
 | 
			
		||||
    $this->drupalGet($edit_path, ['language' => $french]);
 | 
			
		||||
    $this->assertSession()->optionExists('moderation_state[0][state]', 'draft');
 | 
			
		||||
    $this->assertSession()->optionExists('moderation_state[0][state]', 'published');
 | 
			
		||||
    $this->assertSession()->optionExists('moderation_state[0][state]', 'archived');
 | 
			
		||||
 | 
			
		||||
    // Make sure we are allowed to create a pending English revision.
 | 
			
		||||
    $this->drupalGet($edit_path);
 | 
			
		||||
    $this->assertSession()->optionExists('moderation_state[0][state]', 'draft');
 | 
			
		||||
    $this->assertSession()->optionExists('moderation_state[0][state]', 'published');
 | 
			
		||||
    $this->assertSession()->optionExists('moderation_state[0][state]', 'archived');
 | 
			
		||||
 | 
			
		||||
    // Create new moderated content (revision 1).
 | 
			
		||||
    $this->drupalGet('node/add/moderated_content');
 | 
			
		||||
    $this->submitForm([
 | 
			
		||||
      'title[0][value]' => 'Third moderated content',
 | 
			
		||||
      'moderation_state[0][state]' => 'published',
 | 
			
		||||
    ], 'Save');
 | 
			
		||||
 | 
			
		||||
    $node = $this->drupalGetNodeByTitle('Third moderated content');
 | 
			
		||||
    $this->assertNotEmpty($node->language(), 'en');
 | 
			
		||||
    $edit_path = sprintf('node/%d/edit', $node->id());
 | 
			
		||||
    $translate_path = sprintf('node/%d/translations/add/en/fr', $node->id());
 | 
			
		||||
 | 
			
		||||
    // Translate it, without updating data (revision 2).
 | 
			
		||||
    $this->drupalGet($translate_path);
 | 
			
		||||
    $this->assertSession()->optionExists('moderation_state[0][state]', 'draft');
 | 
			
		||||
    $this->assertSession()->optionExists('moderation_state[0][state]', 'published');
 | 
			
		||||
    $this->assertSession()->optionExists('moderation_state[0][state]', 'archived');
 | 
			
		||||
    $this->submitForm([
 | 
			
		||||
      'moderation_state[0][state]' => 'draft',
 | 
			
		||||
    ], 'Save (this translation)');
 | 
			
		||||
 | 
			
		||||
    // Add another draft for the translation (revision 3).
 | 
			
		||||
    $this->drupalGet($edit_path, ['language' => $french]);
 | 
			
		||||
    $this->assertSession()->optionExists('moderation_state[0][state]', 'draft');
 | 
			
		||||
    $this->assertSession()->optionExists('moderation_state[0][state]', 'published');
 | 
			
		||||
    $this->assertSession()->optionNotExists('moderation_state[0][state]', 'archived');
 | 
			
		||||
    $this->submitForm([
 | 
			
		||||
      'moderation_state[0][state]' => 'draft',
 | 
			
		||||
    ], 'Save (this translation)');
 | 
			
		||||
 | 
			
		||||
    // Updating and publishing the french translation is still possible.
 | 
			
		||||
    $this->drupalGet($edit_path, ['language' => $french]);
 | 
			
		||||
    $this->assertSession()->optionExists('moderation_state[0][state]', 'draft');
 | 
			
		||||
    $this->assertSession()->optionExists('moderation_state[0][state]', 'published');
 | 
			
		||||
    $this->assertSession()->optionNotExists('moderation_state[0][state]', 'archived');
 | 
			
		||||
    $this->submitForm([
 | 
			
		||||
      'moderation_state[0][state]' => 'published',
 | 
			
		||||
    ], 'Save (this translation)');
 | 
			
		||||
 | 
			
		||||
    // Now the french translation is published, an english draft can be added.
 | 
			
		||||
    $this->drupalGet($edit_path);
 | 
			
		||||
    $this->assertSession()->optionExists('moderation_state[0][state]', 'draft');
 | 
			
		||||
    $this->assertSession()->optionExists('moderation_state[0][state]', 'published');
 | 
			
		||||
    $this->assertSession()->optionExists('moderation_state[0][state]', 'archived');
 | 
			
		||||
    $this->submitForm([
 | 
			
		||||
      'moderation_state[0][state]' => 'draft',
 | 
			
		||||
    ], 'Save (this translation)');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the moderation_state field when an alternative widget is set.
 | 
			
		||||
   */
 | 
			
		||||
  public function testAlternativeModerationStateWidget(): void {
 | 
			
		||||
    $entity_form_display = EntityFormDisplay::load('node.moderated_content.default');
 | 
			
		||||
    $entity_form_display->setComponent('moderation_state', [
 | 
			
		||||
      'type' => 'string_textfield',
 | 
			
		||||
      'region' => 'content',
 | 
			
		||||
    ]);
 | 
			
		||||
    $entity_form_display->save();
 | 
			
		||||
    $this->drupalGet('node/add/moderated_content');
 | 
			
		||||
    $this->submitForm([
 | 
			
		||||
      'title[0][value]' => 'Test content',
 | 
			
		||||
      'moderation_state[0][value]' => 'published',
 | 
			
		||||
    ], 'Save');
 | 
			
		||||
    $this->assertSession()->pageTextContains('Moderated content Test content has been created.');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests that workflows and states can not be deleted if they are in use.
 | 
			
		||||
   *
 | 
			
		||||
   * @covers \Drupal\content_moderation\Plugin\WorkflowType\ContentModeration::workflowHasData
 | 
			
		||||
   * @covers \Drupal\content_moderation\Plugin\WorkflowType\ContentModeration::workflowStateHasData
 | 
			
		||||
   */
 | 
			
		||||
  public function testWorkflowInUse(): void {
 | 
			
		||||
    $user = $this->createUser([
 | 
			
		||||
      'administer workflows',
 | 
			
		||||
      'create moderated_content content',
 | 
			
		||||
      'edit own moderated_content content',
 | 
			
		||||
      'use editorial transition create_new_draft',
 | 
			
		||||
      'use editorial transition publish',
 | 
			
		||||
      'use editorial transition archive',
 | 
			
		||||
    ]);
 | 
			
		||||
    $this->drupalLogin($user);
 | 
			
		||||
    $paths = [
 | 
			
		||||
      'archived_state' => 'admin/config/workflow/workflows/manage/editorial/state/archived/delete',
 | 
			
		||||
      'editorial_workflow' => 'admin/config/workflow/workflows/manage/editorial/delete',
 | 
			
		||||
    ];
 | 
			
		||||
    $messages = [
 | 
			
		||||
      'archived_state' => 'This workflow state is in use. You cannot remove this workflow state until you have removed all content using it.',
 | 
			
		||||
      'editorial_workflow' => 'This workflow is in use. You cannot remove this workflow until you have removed all content using it.',
 | 
			
		||||
    ];
 | 
			
		||||
    foreach ($paths as $path) {
 | 
			
		||||
      $this->drupalGet($path);
 | 
			
		||||
      $this->assertSession()->buttonExists('Delete');
 | 
			
		||||
    }
 | 
			
		||||
    // Create new moderated content in draft.
 | 
			
		||||
    $this->drupalGet('node/add/moderated_content');
 | 
			
		||||
    $this->submitForm([
 | 
			
		||||
      'title[0][value]' => 'Some moderated content',
 | 
			
		||||
      'body[0][value]' => 'First version of the content.',
 | 
			
		||||
      'moderation_state[0][state]' => 'draft',
 | 
			
		||||
    ], 'Save');
 | 
			
		||||
 | 
			
		||||
    // The archived state is not used yet, so can still be deleted.
 | 
			
		||||
    $this->drupalGet($paths['archived_state']);
 | 
			
		||||
    $this->assertSession()->buttonExists('Delete');
 | 
			
		||||
 | 
			
		||||
    // The workflow is being used, so can't be deleted.
 | 
			
		||||
    $this->drupalGet($paths['editorial_workflow']);
 | 
			
		||||
    $this->assertSession()->buttonNotExists('Delete');
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
    $this->assertSession()->pageTextContains($messages['editorial_workflow']);
 | 
			
		||||
 | 
			
		||||
    $node = $this->drupalGetNodeByTitle('Some moderated content');
 | 
			
		||||
    $this->drupalGet('node/' . $node->id() . '/edit');
 | 
			
		||||
    $this->submitForm(['moderation_state[0][state]' => 'published'], 'Save');
 | 
			
		||||
    $this->drupalGet('node/' . $node->id() . '/edit');
 | 
			
		||||
    $this->submitForm(['moderation_state[0][state]' => 'archived'], 'Save');
 | 
			
		||||
 | 
			
		||||
    // Now the archived state is being used so it can not be deleted either.
 | 
			
		||||
    foreach ($paths as $type => $path) {
 | 
			
		||||
      $this->drupalGet($path);
 | 
			
		||||
      $this->assertSession()->buttonNotExists('Delete');
 | 
			
		||||
      $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
      $this->assertSession()->pageTextContains($messages[$type]);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,682 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\content_moderation\Functional;
 | 
			
		||||
 | 
			
		||||
use Drupal\node\NodeInterface;
 | 
			
		||||
use Drupal\Tests\content_translation\Traits\ContentTranslationTestTrait;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Test content_moderation functionality with localization and translation.
 | 
			
		||||
 *
 | 
			
		||||
 * @group content_moderation
 | 
			
		||||
 * @group #slow
 | 
			
		||||
 */
 | 
			
		||||
class ModerationLocaleTest extends ModerationStateTestBase {
 | 
			
		||||
 | 
			
		||||
  use ContentTranslationTestTrait;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = [
 | 
			
		||||
    'node',
 | 
			
		||||
    'content_moderation',
 | 
			
		||||
    'locale',
 | 
			
		||||
    'content_translation',
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function getAdministratorPermissions(): array {
 | 
			
		||||
    return array_merge($this->permissions, [
 | 
			
		||||
      'create content translations',
 | 
			
		||||
      'bypass node access',
 | 
			
		||||
      'translate any entity',
 | 
			
		||||
    ]);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
 | 
			
		||||
    $this->adminUser = $this->drupalCreateUser($this->getAdministratorPermissions());
 | 
			
		||||
    $this->drupalLogin($this->adminUser);
 | 
			
		||||
    // Enable moderation on Article node type.
 | 
			
		||||
    $this->createContentTypeFromUi('Article', 'article', TRUE);
 | 
			
		||||
 | 
			
		||||
    // Add French and Italian languages.
 | 
			
		||||
    static::createLanguageFromLangcode('fr');
 | 
			
		||||
    static::createLanguageFromLangcode('it');
 | 
			
		||||
 | 
			
		||||
    // Enable content translation on articles.
 | 
			
		||||
    $this->enableContentTranslation('node', 'article');
 | 
			
		||||
 | 
			
		||||
    // Adding languages requires a container rebuild in the test running
 | 
			
		||||
    // environment so that multilingual services are used.
 | 
			
		||||
    $this->rebuildContainer();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests article translations can be moderated separately.
 | 
			
		||||
   */
 | 
			
		||||
  public function testTranslateModeratedContent(): void {
 | 
			
		||||
    // Create a published article in English.
 | 
			
		||||
    $edit = [
 | 
			
		||||
      'title[0][value]' => 'Published English node',
 | 
			
		||||
      'langcode[0][value]' => 'en',
 | 
			
		||||
      'moderation_state[0][state]' => 'published',
 | 
			
		||||
    ];
 | 
			
		||||
    $this->drupalGet('node/add/article');
 | 
			
		||||
    $this->submitForm($edit, 'Save');
 | 
			
		||||
    $this->assertSession()->pageTextContains('Article Published English node has been created.');
 | 
			
		||||
    $english_node = $this->drupalGetNodeByTitle('Published English node');
 | 
			
		||||
 | 
			
		||||
    // Add a French translation.
 | 
			
		||||
    $this->drupalGet('node/' . $english_node->id() . '/translations');
 | 
			
		||||
    $this->clickLink('Add');
 | 
			
		||||
    $edit = [
 | 
			
		||||
      'title[0][value]' => 'French node Draft',
 | 
			
		||||
      'moderation_state[0][state]' => 'draft',
 | 
			
		||||
    ];
 | 
			
		||||
    $this->submitForm($edit, 'Save (this translation)');
 | 
			
		||||
    // Here the error has occurred "The website encountered an unexpected error.
 | 
			
		||||
    // Try again later."
 | 
			
		||||
    // If the translation has got lost.
 | 
			
		||||
    $this->assertSession()->pageTextContains('Article French node Draft has been updated.');
 | 
			
		||||
 | 
			
		||||
    // Create an article in English.
 | 
			
		||||
    $edit = [
 | 
			
		||||
      'title[0][value]' => 'English node',
 | 
			
		||||
      'langcode[0][value]' => 'en',
 | 
			
		||||
      'moderation_state[0][state]' => 'draft',
 | 
			
		||||
    ];
 | 
			
		||||
    $this->drupalGet('node/add/article');
 | 
			
		||||
    $this->submitForm($edit, 'Save');
 | 
			
		||||
    $this->assertSession()->pageTextContains('Article English node has been created.');
 | 
			
		||||
    $english_node = $this->drupalGetNodeByTitle('English node');
 | 
			
		||||
 | 
			
		||||
    // Add a French translation.
 | 
			
		||||
    $this->drupalGet('node/' . $english_node->id() . '/translations');
 | 
			
		||||
    $this->clickLink('Add');
 | 
			
		||||
    $edit = [
 | 
			
		||||
      'title[0][value]' => 'French node',
 | 
			
		||||
      'moderation_state[0][state]' => 'draft',
 | 
			
		||||
    ];
 | 
			
		||||
    $this->submitForm($edit, 'Save (this translation)');
 | 
			
		||||
    $this->assertSession()->pageTextContains('Article French node has been updated.');
 | 
			
		||||
    $english_node = $this->drupalGetNodeByTitle('English node', TRUE);
 | 
			
		||||
 | 
			
		||||
    // Publish the English article and check that the translation stays
 | 
			
		||||
    // unpublished.
 | 
			
		||||
    $this->drupalGet('node/' . $english_node->id() . '/edit');
 | 
			
		||||
    $this->submitForm(['moderation_state[0][state]' => 'published'], 'Save (this translation)');
 | 
			
		||||
    $this->assertSession()->pageTextContains('Article English node has been updated.');
 | 
			
		||||
    $english_node = $this->drupalGetNodeByTitle('English node', TRUE);
 | 
			
		||||
    $french_node = $english_node->getTranslation('fr');
 | 
			
		||||
    $this->assertEquals('French node', $french_node->label());
 | 
			
		||||
 | 
			
		||||
    $this->assertEquals('published', $english_node->moderation_state->value);
 | 
			
		||||
    $this->assertTrue($english_node->isPublished());
 | 
			
		||||
    $this->assertEquals('draft', $french_node->moderation_state->value);
 | 
			
		||||
    $this->assertFalse($french_node->isPublished());
 | 
			
		||||
 | 
			
		||||
    // Create another article with its translation. This time we will publish
 | 
			
		||||
    // the translation first.
 | 
			
		||||
    $edit = [
 | 
			
		||||
      'title[0][value]' => 'Another node',
 | 
			
		||||
      'moderation_state[0][state]' => 'draft',
 | 
			
		||||
    ];
 | 
			
		||||
    $this->drupalGet('node/add/article');
 | 
			
		||||
    $this->submitForm($edit, 'Save');
 | 
			
		||||
    $this->assertSession()->pageTextContains('Article Another node has been created.');
 | 
			
		||||
    $english_node = $this->drupalGetNodeByTitle('Another node');
 | 
			
		||||
 | 
			
		||||
    // Add a French translation.
 | 
			
		||||
    $this->drupalGet('node/' . $english_node->id() . '/translations');
 | 
			
		||||
    $this->clickLink('Add');
 | 
			
		||||
    $edit = [
 | 
			
		||||
      'title[0][value]' => 'Translated node',
 | 
			
		||||
      'moderation_state[0][state]' => 'draft',
 | 
			
		||||
    ];
 | 
			
		||||
    $this->submitForm($edit, 'Save (this translation)');
 | 
			
		||||
    $this->assertSession()->pageTextContains('Article Translated node has been updated.');
 | 
			
		||||
    $english_node = $this->drupalGetNodeByTitle('Another node', TRUE);
 | 
			
		||||
 | 
			
		||||
    // Publish the translation and check that the source language version stays
 | 
			
		||||
    // unpublished.
 | 
			
		||||
    $this->drupalGet('fr/node/' . $english_node->id() . '/edit');
 | 
			
		||||
    $this->submitForm(['moderation_state[0][state]' => 'published'], 'Save (this translation)');
 | 
			
		||||
    $this->assertSession()->pageTextContains('Article Translated node has been updated.');
 | 
			
		||||
    $english_node = $this->drupalGetNodeByTitle('Another node', TRUE);
 | 
			
		||||
    $french_node = $english_node->getTranslation('fr');
 | 
			
		||||
    $this->assertEquals('published', $french_node->moderation_state->value);
 | 
			
		||||
    $this->assertTrue($french_node->isPublished());
 | 
			
		||||
    $this->assertEquals('draft', $english_node->moderation_state->value);
 | 
			
		||||
    $this->assertFalse($english_node->isPublished());
 | 
			
		||||
 | 
			
		||||
    // Now check that we can create a new draft of the translation.
 | 
			
		||||
    $edit = [
 | 
			
		||||
      'title[0][value]' => 'New draft of translated node',
 | 
			
		||||
      'moderation_state[0][state]' => 'draft',
 | 
			
		||||
    ];
 | 
			
		||||
    $this->drupalGet('fr/node/' . $english_node->id() . '/edit');
 | 
			
		||||
    $this->submitForm($edit, 'Save (this translation)');
 | 
			
		||||
    $this->assertSession()->pageTextContains('Article New draft of translated node has been updated.');
 | 
			
		||||
    $english_node = $this->drupalGetNodeByTitle('Another node', TRUE);
 | 
			
		||||
    $french_node = $english_node->getTranslation('fr');
 | 
			
		||||
    $this->assertEquals('published', $french_node->moderation_state->value);
 | 
			
		||||
    $this->assertTrue($french_node->isPublished());
 | 
			
		||||
    $this->assertEquals('Translated node', $french_node->getTitle(), 'The default revision of the published translation remains the same.');
 | 
			
		||||
 | 
			
		||||
    // Publish the French article before testing the archive transition.
 | 
			
		||||
    $this->drupalGet('fr/node/' . $english_node->id() . '/edit');
 | 
			
		||||
    $this->submitForm(['moderation_state[0][state]' => 'published'], 'Save (this translation)');
 | 
			
		||||
    $this->assertSession()->pageTextContains('Article New draft of translated node has been updated.');
 | 
			
		||||
    $english_node = $this->drupalGetNodeByTitle('Another node', TRUE);
 | 
			
		||||
    $french_node = $english_node->getTranslation('fr');
 | 
			
		||||
    $this->assertEquals('published', $french_node->moderation_state->value);
 | 
			
		||||
    $this->assertTrue($french_node->isPublished());
 | 
			
		||||
    $this->assertEquals('New draft of translated node', $french_node->getTitle(), 'The draft has replaced the published revision.');
 | 
			
		||||
 | 
			
		||||
    // Publish the English article before testing the archive transition.
 | 
			
		||||
    $this->drupalGet('node/' . $english_node->id() . '/edit');
 | 
			
		||||
    $this->submitForm([
 | 
			
		||||
      'moderation_state[0][state]' => 'published',
 | 
			
		||||
    ], 'Save (this translation)');
 | 
			
		||||
    $this->assertSession()->pageTextContains('Article Another node has been updated.');
 | 
			
		||||
    $english_node = $this->drupalGetNodeByTitle('Another node', TRUE);
 | 
			
		||||
    $this->assertEquals('published', $english_node->moderation_state->value);
 | 
			
		||||
 | 
			
		||||
    // Archive the node and its translation.
 | 
			
		||||
    $this->drupalGet('node/' . $english_node->id() . '/edit');
 | 
			
		||||
    $this->submitForm([
 | 
			
		||||
      'moderation_state[0][state]' => 'archived',
 | 
			
		||||
    ], 'Save (this translation)');
 | 
			
		||||
    $this->assertSession()->pageTextContains('Article Another node has been updated.');
 | 
			
		||||
    $this->drupalGet('fr/node/' . $english_node->id() . '/edit');
 | 
			
		||||
    $this->submitForm([
 | 
			
		||||
      'moderation_state[0][state]' => 'archived',
 | 
			
		||||
    ], 'Save (this translation)');
 | 
			
		||||
    $this->assertSession()->pageTextContains('Article New draft of translated node has been updated.');
 | 
			
		||||
    $english_node = $this->drupalGetNodeByTitle('Another node', TRUE);
 | 
			
		||||
    $french_node = $english_node->getTranslation('fr');
 | 
			
		||||
    $this->assertEquals('archived', $english_node->moderation_state->value);
 | 
			
		||||
    $this->assertFalse($english_node->isPublished());
 | 
			
		||||
    $this->assertEquals('archived', $french_node->moderation_state->value);
 | 
			
		||||
    $this->assertFalse($french_node->isPublished());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests that individual translations can be moderated independently.
 | 
			
		||||
   */
 | 
			
		||||
  public function testLanguageIndependentContentModeration(): void {
 | 
			
		||||
    // Create a published article in English (revision 1).
 | 
			
		||||
    $this->drupalGet('node/add/article');
 | 
			
		||||
    $node = $this->submitNodeForm('Test 1.1 EN', 'published');
 | 
			
		||||
    $this->assertNotLatestVersionPage($node);
 | 
			
		||||
 | 
			
		||||
    $edit_path = $node->toUrl('edit-form');
 | 
			
		||||
    $translate_path = $node->toUrl('drupal:content-translation-overview');
 | 
			
		||||
 | 
			
		||||
    // Create a new English draft (revision 2).
 | 
			
		||||
    $this->drupalGet($edit_path);
 | 
			
		||||
    $this->submitNodeForm('Test 1.2 EN', 'draft', TRUE);
 | 
			
		||||
    $this->assertLatestVersionPage($node);
 | 
			
		||||
 | 
			
		||||
    // Add a French translation draft (revision 3).
 | 
			
		||||
    $this->drupalGet($translate_path);
 | 
			
		||||
    $this->clickLink('Add');
 | 
			
		||||
    $this->submitNodeForm('Test 1.3 FR', 'draft');
 | 
			
		||||
    $fr_node = $this->loadTranslation($node, 'fr');
 | 
			
		||||
    $this->assertLatestVersionPage($fr_node);
 | 
			
		||||
    $this->assertModerationForm($node);
 | 
			
		||||
 | 
			
		||||
    // Add an Italian translation draft (revision 4).
 | 
			
		||||
    $this->drupalGet($translate_path);
 | 
			
		||||
    $this->clickLink('Add');
 | 
			
		||||
    $this->submitNodeForm('Test 1.4 IT', 'draft');
 | 
			
		||||
    $it_node = $this->loadTranslation($node, 'it');
 | 
			
		||||
    $this->assertLatestVersionPage($it_node);
 | 
			
		||||
    $this->assertModerationForm($node);
 | 
			
		||||
    $this->assertModerationForm($fr_node);
 | 
			
		||||
 | 
			
		||||
    // Publish the English draft (revision 5).
 | 
			
		||||
    $this->drupalGet($edit_path);
 | 
			
		||||
    $this->submitNodeForm('Test 1.5 EN', 'published', TRUE);
 | 
			
		||||
    $this->assertNotLatestVersionPage($node);
 | 
			
		||||
    $this->assertModerationForm($fr_node);
 | 
			
		||||
    $this->assertModerationForm($it_node);
 | 
			
		||||
 | 
			
		||||
    // Publish the Italian draft (revision 6).
 | 
			
		||||
    $this->drupalGet($translate_path);
 | 
			
		||||
    $this->clickLink('Edit', 3);
 | 
			
		||||
    $this->submitNodeForm('Test 1.6 IT', 'published');
 | 
			
		||||
    $this->assertNotLatestVersionPage($it_node);
 | 
			
		||||
    $this->assertNoModerationForm($node);
 | 
			
		||||
    $this->assertModerationForm($fr_node);
 | 
			
		||||
 | 
			
		||||
    // Publish the French draft (revision 7).
 | 
			
		||||
    $this->drupalGet($translate_path);
 | 
			
		||||
    $this->clickLink('Edit', 2);
 | 
			
		||||
    $this->submitNodeForm('Test 1.7 FR', 'published');
 | 
			
		||||
    $this->assertNotLatestVersionPage($fr_node);
 | 
			
		||||
    $this->assertNoModerationForm($node);
 | 
			
		||||
    $this->assertNoModerationForm($it_node);
 | 
			
		||||
 | 
			
		||||
    // Create an Italian draft (revision 8).
 | 
			
		||||
    $this->drupalGet($translate_path);
 | 
			
		||||
    $this->clickLink('Edit', 3);
 | 
			
		||||
    $this->submitNodeForm('Test 1.8 IT', 'draft');
 | 
			
		||||
    $this->assertLatestVersionPage($it_node);
 | 
			
		||||
    $this->assertNoModerationForm($node);
 | 
			
		||||
    $this->assertNoModerationForm($fr_node);
 | 
			
		||||
 | 
			
		||||
    // Create a French draft (revision 9).
 | 
			
		||||
    $this->drupalGet($translate_path);
 | 
			
		||||
    $this->clickLink('Edit', 2);
 | 
			
		||||
    $this->submitNodeForm('Test 1.9 FR', 'draft');
 | 
			
		||||
    $this->assertLatestVersionPage($fr_node);
 | 
			
		||||
    $this->assertNoModerationForm($node);
 | 
			
		||||
    $this->assertModerationForm($it_node);
 | 
			
		||||
 | 
			
		||||
    // Create an English draft (revision 10).
 | 
			
		||||
    $this->drupalGet($edit_path);
 | 
			
		||||
    $this->submitNodeForm('Test 1.10 EN', 'draft');
 | 
			
		||||
    $this->assertLatestVersionPage($node);
 | 
			
		||||
    $this->assertModerationForm($fr_node);
 | 
			
		||||
    $this->assertModerationForm($it_node);
 | 
			
		||||
 | 
			
		||||
    // Now start from a draft article in English (revision 1).
 | 
			
		||||
    $this->drupalGet('node/add/article');
 | 
			
		||||
    $node2 = $this->submitNodeForm('Test 2.1 EN', 'draft', TRUE);
 | 
			
		||||
    $this->assertNotLatestVersionPage($node2, TRUE);
 | 
			
		||||
 | 
			
		||||
    $edit_path = $node2->toUrl('edit-form');
 | 
			
		||||
    $translate_path = $node2->toUrl('drupal:content-translation-overview');
 | 
			
		||||
 | 
			
		||||
    // Add a French translation (revision 2).
 | 
			
		||||
    $this->drupalGet($translate_path);
 | 
			
		||||
    $this->clickLink('Add');
 | 
			
		||||
    $this->submitNodeForm('Test 2.2 FR', 'draft');
 | 
			
		||||
    $fr_node2 = $this->loadTranslation($node2, 'fr');
 | 
			
		||||
    $this->assertNotLatestVersionPage($fr_node2, TRUE);
 | 
			
		||||
    $this->assertModerationForm($node2, FALSE);
 | 
			
		||||
 | 
			
		||||
    // Add an Italian translation (revision 3).
 | 
			
		||||
    $this->drupalGet($translate_path);
 | 
			
		||||
    $this->clickLink('Add');
 | 
			
		||||
    $this->submitNodeForm('Test 2.3 IT', 'draft');
 | 
			
		||||
    $it_node2 = $this->loadTranslation($node2, 'it');
 | 
			
		||||
    $this->assertNotLatestVersionPage($it_node2, TRUE);
 | 
			
		||||
    $this->assertModerationForm($node2, FALSE);
 | 
			
		||||
    $this->assertModerationForm($fr_node2, FALSE);
 | 
			
		||||
 | 
			
		||||
    // Publish the English draft (revision 4).
 | 
			
		||||
    $this->drupalGet($edit_path);
 | 
			
		||||
    $this->submitNodeForm('Test 2.4 EN', 'published', TRUE);
 | 
			
		||||
    $this->assertNotLatestVersionPage($node2);
 | 
			
		||||
    $this->assertModerationForm($fr_node2, FALSE);
 | 
			
		||||
    $this->assertModerationForm($it_node2, FALSE);
 | 
			
		||||
 | 
			
		||||
    // Publish the Italian draft (revision 5).
 | 
			
		||||
    $this->drupalGet($translate_path);
 | 
			
		||||
    $this->clickLink('Edit', 3);
 | 
			
		||||
    $this->submitNodeForm('Test 2.5 IT', 'published');
 | 
			
		||||
    $this->assertNotLatestVersionPage($it_node2);
 | 
			
		||||
    $this->assertNoModerationForm($node2);
 | 
			
		||||
    $this->assertModerationForm($fr_node2, FALSE);
 | 
			
		||||
 | 
			
		||||
    // Publish the French draft (revision 6).
 | 
			
		||||
    $this->drupalGet($translate_path);
 | 
			
		||||
    $this->clickLink('Edit', 2);
 | 
			
		||||
    $this->submitNodeForm('Test 2.6 FR', 'published');
 | 
			
		||||
    $this->assertNotLatestVersionPage($fr_node2);
 | 
			
		||||
    $this->assertNoModerationForm($node2);
 | 
			
		||||
    $this->assertNoModerationForm($it_node2);
 | 
			
		||||
 | 
			
		||||
    // Now that all revision translations are published, verify that the
 | 
			
		||||
    // moderation form is never displayed on revision pages.
 | 
			
		||||
    /** @var \Drupal\node\NodeStorageInterface $storage */
 | 
			
		||||
    $storage = $this->container->get('entity_type.manager')->getStorage('node');
 | 
			
		||||
    foreach (range(11, 16) as $revision_id) {
 | 
			
		||||
      /** @var \Drupal\node\NodeInterface $revision */
 | 
			
		||||
      $revision = $storage->loadRevision($revision_id);
 | 
			
		||||
      foreach (array_keys($revision->getTranslationLanguages()) as $langcode) {
 | 
			
		||||
        if ($revision->isRevisionTranslationAffected()) {
 | 
			
		||||
          $translation = $revision->getTranslation($langcode);
 | 
			
		||||
          $this->drupalGet($translation->toUrl('revision'));
 | 
			
		||||
          $this->assertFalse($this->hasModerationForm(), 'Moderation form is not displayed correctly for revision ' . $revision_id);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Create an Italian draft (revision 7).
 | 
			
		||||
    $this->drupalGet($translate_path);
 | 
			
		||||
    $this->clickLink('Edit', 3);
 | 
			
		||||
    $this->submitNodeForm('Test 2.7 IT', 'draft');
 | 
			
		||||
    $this->assertLatestVersionPage($it_node2);
 | 
			
		||||
    $this->assertNoModerationForm($node2);
 | 
			
		||||
    $this->assertNoModerationForm($fr_node2);
 | 
			
		||||
 | 
			
		||||
    // Create a French draft (revision 8).
 | 
			
		||||
    $this->drupalGet($translate_path);
 | 
			
		||||
    $this->clickLink('Edit', 2);
 | 
			
		||||
    $this->submitNodeForm('Test 2.8 FR', 'draft');
 | 
			
		||||
    $this->assertLatestVersionPage($fr_node2);
 | 
			
		||||
    $this->assertNoModerationForm($node2);
 | 
			
		||||
    $this->assertModerationForm($it_node2);
 | 
			
		||||
 | 
			
		||||
    // Create an English draft (revision 9).
 | 
			
		||||
    $this->drupalGet($edit_path);
 | 
			
		||||
    $this->submitNodeForm('Test 2.9 EN', 'draft', TRUE);
 | 
			
		||||
    $this->assertLatestVersionPage($node2);
 | 
			
		||||
    $this->assertModerationForm($fr_node2);
 | 
			
		||||
    $this->assertModerationForm($it_node2);
 | 
			
		||||
 | 
			
		||||
    // Now publish a draft in another language first and verify that the
 | 
			
		||||
    // moderation form is not displayed on the English node view page.
 | 
			
		||||
    $this->drupalGet('node/add/article');
 | 
			
		||||
    $node3 = $this->submitNodeForm('Test 3.1 EN', 'published');
 | 
			
		||||
    $this->assertNotLatestVersionPage($node3);
 | 
			
		||||
 | 
			
		||||
    $edit_path = $node3->toUrl('edit-form');
 | 
			
		||||
    $translate_path = $node3->toUrl('drupal:content-translation-overview');
 | 
			
		||||
 | 
			
		||||
    // Create an English draft (revision 2).
 | 
			
		||||
    $this->drupalGet($edit_path);
 | 
			
		||||
    $this->submitNodeForm('Test 3.2 EN', 'draft', TRUE);
 | 
			
		||||
    $this->assertLatestVersionPage($node3);
 | 
			
		||||
 | 
			
		||||
    // Add a French translation (revision 3).
 | 
			
		||||
    $this->drupalGet($translate_path);
 | 
			
		||||
    $this->clickLink('Add');
 | 
			
		||||
    $this->submitNodeForm('Test 3.3 FR', 'draft');
 | 
			
		||||
    $fr_node3 = $this->loadTranslation($node3, 'fr');
 | 
			
		||||
    $this->assertLatestVersionPage($fr_node3);
 | 
			
		||||
    $this->assertModerationForm($node3);
 | 
			
		||||
 | 
			
		||||
    // Publish the French draft (revision 4).
 | 
			
		||||
    $this->drupalGet($translate_path);
 | 
			
		||||
    $this->clickLink('Edit', 2);
 | 
			
		||||
    $this->submitNodeForm('Test 3.4 FR', 'published');
 | 
			
		||||
    $this->assertNotLatestVersionPage($fr_node3);
 | 
			
		||||
    $this->assertModerationForm($node3);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Checks that new translation values are populated properly.
 | 
			
		||||
   */
 | 
			
		||||
  public function testNewTranslationSourceValues(): void {
 | 
			
		||||
    // Create a published article in Italian (revision 1).
 | 
			
		||||
    $this->drupalGet('node/add/article');
 | 
			
		||||
    $node = $this->submitNodeForm('Test 1.1 IT', 'published', TRUE, 'it');
 | 
			
		||||
    $this->assertNotLatestVersionPage($node);
 | 
			
		||||
 | 
			
		||||
    // Create a new draft (revision 2).
 | 
			
		||||
    $this->drupalGet($node->toUrl('edit-form'));
 | 
			
		||||
    $this->submitNodeForm('Test 1.2 IT', 'draft', TRUE);
 | 
			
		||||
    $this->assertLatestVersionPage($node);
 | 
			
		||||
 | 
			
		||||
    // Create an English draft (revision 3) and verify that the Italian draft
 | 
			
		||||
    // values are used as source values.
 | 
			
		||||
    $url = $node->toUrl('drupal:content-translation-add');
 | 
			
		||||
    $url->setRouteParameter('source', 'it');
 | 
			
		||||
    $url->setRouteParameter('target', 'en');
 | 
			
		||||
    $this->drupalGet($url);
 | 
			
		||||
    $this->assertSession()->pageTextContains('Test 1.2 IT');
 | 
			
		||||
    $this->submitNodeForm('Test 1.3 EN', 'draft');
 | 
			
		||||
    $this->assertLatestVersionPage($node);
 | 
			
		||||
 | 
			
		||||
    // Create a French draft (without saving) and verify that the Italian draft
 | 
			
		||||
    // values are used as source values.
 | 
			
		||||
    $url->setRouteParameter('target', 'fr');
 | 
			
		||||
    $this->drupalGet($url);
 | 
			
		||||
    $this->assertSession()->pageTextContains('Test 1.2 IT');
 | 
			
		||||
 | 
			
		||||
    // Now switch source language and verify that the English draft values are
 | 
			
		||||
    // used as source values.
 | 
			
		||||
    $url->setRouteParameter('source', 'en');
 | 
			
		||||
    $this->drupalGet($url);
 | 
			
		||||
    $this->assertSession()->pageTextContains('Test 1.3 EN');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests article revision history shows revisions for the correct translation.
 | 
			
		||||
   */
 | 
			
		||||
  public function testTranslationRevisionsHistory(): void {
 | 
			
		||||
    // Create a published article in English.
 | 
			
		||||
    $edit = [
 | 
			
		||||
      'title[0][value]' => 'English node',
 | 
			
		||||
      'langcode[0][value]' => 'en',
 | 
			
		||||
      'moderation_state[0][state]' => 'published',
 | 
			
		||||
      'revision_log[0][value]' => 'Log Message - English - Published - Edit 1',
 | 
			
		||||
    ];
 | 
			
		||||
    $this->drupalGet('node/add/article');
 | 
			
		||||
    $this->submitForm($edit, 'Save');
 | 
			
		||||
    $this->assertSession()->pageTextContains('Article English node has been created.');
 | 
			
		||||
    $node = $this->drupalGetNodeByTitle('English node');
 | 
			
		||||
 | 
			
		||||
    // Add a French translation.
 | 
			
		||||
    $this->drupalGet('node/' . $node->id() . '/translations');
 | 
			
		||||
    $this->clickLink('Add');
 | 
			
		||||
    $edit = [
 | 
			
		||||
      'title[0][value]' => 'French node',
 | 
			
		||||
      'moderation_state[0][state]' => 'draft',
 | 
			
		||||
      'revision_log[0][value]' => 'Log Message - French - Draft - Edit 1',
 | 
			
		||||
    ];
 | 
			
		||||
    $this->submitForm($edit, 'Save (this translation)');
 | 
			
		||||
    // Here the error has occurred "The website encountered an unexpected error.
 | 
			
		||||
    // Try again later."
 | 
			
		||||
    // If the translation has got lost.
 | 
			
		||||
    $this->assertSession()->pageTextContains('Article French node has been updated.');
 | 
			
		||||
    $french_node = $this->loadTranslation($node, 'fr');
 | 
			
		||||
    $this->assertEquals('published', $node->moderation_state->value);
 | 
			
		||||
    $this->assertTrue($node->isPublished());
 | 
			
		||||
    $this->assertEquals('draft', $french_node->moderation_state->value);
 | 
			
		||||
    $this->assertFalse($french_node->isPublished());
 | 
			
		||||
 | 
			
		||||
    // Verify the revisions history for the English node.
 | 
			
		||||
    $this->drupalGet('node/' . $node->id() . '/revisions');
 | 
			
		||||
    $this->assertSession()->pageTextContains('Log Message - English - Published - Edit 1');
 | 
			
		||||
    $this->assertSession()->pageTextNotContains('Log Message - French');
 | 
			
		||||
 | 
			
		||||
    // Verify the revisions history for the French node.
 | 
			
		||||
    $this->drupalGet($french_node->language()->getId() . '/node/' . $node->id() . '/revisions');
 | 
			
		||||
    $this->assertSession()->pageTextContains('Log Message - French - Draft - Edit 1');
 | 
			
		||||
    $this->assertSession()->pageTextNotContains('Log Message - English');
 | 
			
		||||
 | 
			
		||||
    // Create a new draft for the English article.
 | 
			
		||||
    $edit = [
 | 
			
		||||
      'moderation_state[0][state]' => 'draft',
 | 
			
		||||
      'revision_log[0][value]' => 'Log Message - English - Draft - Edit 2',
 | 
			
		||||
    ];
 | 
			
		||||
    $this->drupalGet('node/' . $node->id() . '/edit');
 | 
			
		||||
    $this->submitForm($edit, 'Save');
 | 
			
		||||
    $this->assertSession()->pageTextContains('Article English node has been updated.');
 | 
			
		||||
 | 
			
		||||
    // Create a new draft for the French article.
 | 
			
		||||
    $edit = [
 | 
			
		||||
      'moderation_state[0][state]' => 'draft',
 | 
			
		||||
      'revision_log[0][value]' => 'Log Message - French - Draft - Edit 2',
 | 
			
		||||
    ];
 | 
			
		||||
    $this->drupalGet($french_node->language()->getId() . '/node/' . $node->id() . '/edit');
 | 
			
		||||
    $this->submitForm($edit, 'Save (this translation)');
 | 
			
		||||
    $this->assertSession()->pageTextContains('Article French node has been updated.');
 | 
			
		||||
 | 
			
		||||
    // Verify the revisions history for the English node.
 | 
			
		||||
    $this->drupalGet('node/' . $node->id() . '/revisions');
 | 
			
		||||
    $this->assertSession()->pageTextContains('Log Message - English - Published - Edit 1');
 | 
			
		||||
    $this->assertSession()->pageTextContains('Log Message - English - Draft - Edit 2');
 | 
			
		||||
    $this->assertSession()->pageTextNotContains('Log Message - French');
 | 
			
		||||
 | 
			
		||||
    // Verify the revisions history for the French node.
 | 
			
		||||
    $this->drupalGet($french_node->language()->getId() . '/node/' . $node->id() . '/revisions');
 | 
			
		||||
    $this->assertSession()->pageTextContains('Log Message - French - Draft - Edit 1');
 | 
			
		||||
    $this->assertSession()->pageTextContains('Log Message - French - Draft - Edit 2');
 | 
			
		||||
    $this->assertSession()->pageTextNotContains('Log Message - English');
 | 
			
		||||
 | 
			
		||||
    // Publish the French Node.
 | 
			
		||||
    $edit = [
 | 
			
		||||
      'moderation_state[0][state]' => 'published',
 | 
			
		||||
      'revision_log[0][value]' => 'Log Message - French - Published - Edit 3',
 | 
			
		||||
    ];
 | 
			
		||||
    $this->drupalGet($french_node->language()->getId() . '/node/' . $node->id() . '/edit');
 | 
			
		||||
    $this->submitForm($edit, 'Save (this translation)');
 | 
			
		||||
    $this->assertSession()->pageTextContains('Article French node has been updated.');
 | 
			
		||||
 | 
			
		||||
    // Verify the revisions history for the English node.
 | 
			
		||||
    $this->drupalGet('node/' . $node->id() . '/revisions');
 | 
			
		||||
    $this->assertSession()->pageTextContains('Log Message - English - Published - Edit 1');
 | 
			
		||||
    $this->assertSession()->pageTextContains('Log Message - English - Draft - Edit 2');
 | 
			
		||||
    $this->assertSession()->pageTextNotContains('Log Message - French');
 | 
			
		||||
 | 
			
		||||
    // Verify the revisions history for the French node.
 | 
			
		||||
    $this->drupalGet($french_node->language()->getId() . '/node/' . $node->id() . '/revisions');
 | 
			
		||||
    $this->assertSession()->pageTextContains('Log Message - French - Draft - Edit 1');
 | 
			
		||||
    $this->assertSession()->pageTextContains('Log Message - French - Draft - Edit 2');
 | 
			
		||||
    $this->assertSession()->pageTextContains('Log Message - French - Published - Edit 3');
 | 
			
		||||
    $this->assertSession()->pageTextNotContains('Log Message - English');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Submits the node form at the current URL with the specified values.
 | 
			
		||||
   *
 | 
			
		||||
   * @param string $title
 | 
			
		||||
   *   The node title.
 | 
			
		||||
   * @param string $moderation_state
 | 
			
		||||
   *   The moderation state.
 | 
			
		||||
   * @param bool $default_translation
 | 
			
		||||
   *   (optional) Whether we are editing the default translation.
 | 
			
		||||
   * @param string|null $langcode
 | 
			
		||||
   *   (optional) The node language. Defaults to English.
 | 
			
		||||
   *
 | 
			
		||||
   * @return \Drupal\node\NodeInterface|null
 | 
			
		||||
   *   A node object if a new one is being created, NULL otherwise.
 | 
			
		||||
   */
 | 
			
		||||
  protected function submitNodeForm($title, $moderation_state, $default_translation = FALSE, $langcode = 'en') {
 | 
			
		||||
    $is_new = str_contains($this->getSession()->getCurrentUrl(), '/node/add/');
 | 
			
		||||
    $edit = [
 | 
			
		||||
      'title[0][value]' => $title,
 | 
			
		||||
      'moderation_state[0][state]' => $moderation_state,
 | 
			
		||||
    ];
 | 
			
		||||
    if ($is_new) {
 | 
			
		||||
      $default_translation = TRUE;
 | 
			
		||||
      $edit['langcode[0][value]'] = $langcode;
 | 
			
		||||
    }
 | 
			
		||||
    $submit = $default_translation ? 'Save' : 'Save (this translation)';
 | 
			
		||||
    $this->submitForm($edit, $submit);
 | 
			
		||||
    $message = $is_new ? "Article $title has been created." : "Article $title has been updated.";
 | 
			
		||||
    $this->assertSession()->pageTextContains($message);
 | 
			
		||||
    return $is_new ? $this->drupalGetNodeByTitle($title) : NULL;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Loads the node translation for the specified language.
 | 
			
		||||
   *
 | 
			
		||||
   * @param \Drupal\node\NodeInterface $node
 | 
			
		||||
   *   A node object.
 | 
			
		||||
   * @param string $langcode
 | 
			
		||||
   *   The translation language code.
 | 
			
		||||
   *
 | 
			
		||||
   * @return \Drupal\node\NodeInterface
 | 
			
		||||
   *   The node translation object.
 | 
			
		||||
   */
 | 
			
		||||
  protected function loadTranslation(NodeInterface $node, $langcode) {
 | 
			
		||||
    /** @var \Drupal\node\NodeStorageInterface $storage */
 | 
			
		||||
    $storage = $this->container->get('entity_type.manager')->getStorage('node');
 | 
			
		||||
    // Explicitly invalidate the cache for that node, as the call below is
 | 
			
		||||
    // statically cached.
 | 
			
		||||
    $storage->resetCache([$node->id()]);
 | 
			
		||||
    /** @var \Drupal\node\NodeInterface $node */
 | 
			
		||||
    $node = $storage->loadRevision($storage->getLatestRevisionId($node->id()));
 | 
			
		||||
    return $node->getTranslation($langcode);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Asserts that this is the "latest version" page for the specified node.
 | 
			
		||||
   *
 | 
			
		||||
   * @param \Drupal\node\NodeInterface $node
 | 
			
		||||
   *   A node object.
 | 
			
		||||
   *
 | 
			
		||||
   * @internal
 | 
			
		||||
   */
 | 
			
		||||
  public function assertLatestVersionPage(NodeInterface $node): void {
 | 
			
		||||
    $this->assertEquals($node->toUrl('latest-version')->setAbsolute()->toString(), $this->getSession()->getCurrentUrl());
 | 
			
		||||
    $this->assertModerationForm($node);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Asserts that this is not the "latest version" page for the specified node.
 | 
			
		||||
   *
 | 
			
		||||
   * @param \Drupal\node\NodeInterface $node
 | 
			
		||||
   *   A node object.
 | 
			
		||||
   * @param bool $moderation_form
 | 
			
		||||
   *   (optional) Whether the page should contain the moderation form. Defaults
 | 
			
		||||
   *   to FALSE.
 | 
			
		||||
   *
 | 
			
		||||
   * @internal
 | 
			
		||||
   */
 | 
			
		||||
  public function assertNotLatestVersionPage(NodeInterface $node, bool $moderation_form = FALSE): void {
 | 
			
		||||
    $this->assertNotEquals($node->toUrl('latest-version')->setAbsolute()->toString(), $this->getSession()->getCurrentUrl());
 | 
			
		||||
    if ($moderation_form) {
 | 
			
		||||
      $this->assertModerationForm($node, FALSE);
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
      $this->assertNoModerationForm($node);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Asserts that the moderation form is displayed for the specified node.
 | 
			
		||||
   *
 | 
			
		||||
   * @param \Drupal\node\NodeInterface $node
 | 
			
		||||
   *   A node object.
 | 
			
		||||
   * @param bool $latest_tab
 | 
			
		||||
   *   (optional) Whether the node form is expected to be displayed on the
 | 
			
		||||
   *   latest version page or on the node view page. Defaults to the former.
 | 
			
		||||
   *
 | 
			
		||||
   * @internal
 | 
			
		||||
   */
 | 
			
		||||
  public function assertModerationForm(NodeInterface $node, bool $latest_tab = TRUE): void {
 | 
			
		||||
    $this->drupalGet($node->toUrl());
 | 
			
		||||
    $this->assertEquals(!$latest_tab, $this->hasModerationForm());
 | 
			
		||||
    $this->drupalGet($node->toUrl('latest-version'));
 | 
			
		||||
    $this->assertEquals($latest_tab, $this->hasModerationForm());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Asserts that the moderation form is not displayed for the specified node.
 | 
			
		||||
   *
 | 
			
		||||
   * @param \Drupal\node\NodeInterface $node
 | 
			
		||||
   *   A node object.
 | 
			
		||||
   *
 | 
			
		||||
   * @internal
 | 
			
		||||
   */
 | 
			
		||||
  public function assertNoModerationForm(NodeInterface $node): void {
 | 
			
		||||
    $this->drupalGet($node->toUrl());
 | 
			
		||||
    $this->assertFalse($this->hasModerationForm());
 | 
			
		||||
    $this->drupalGet($node->toUrl('latest-version'));
 | 
			
		||||
    $this->assertEquals(403, $this->getSession()->getStatusCode());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Checks whether the page contains the moderation form.
 | 
			
		||||
   *
 | 
			
		||||
   * @return bool
 | 
			
		||||
   *   TRUE if the moderation form could be find in the page, FALSE otherwise.
 | 
			
		||||
   */
 | 
			
		||||
  public function hasModerationForm() {
 | 
			
		||||
    return (bool) $this->xpath('//ul[@class="entity-moderation-form"]');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,108 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\content_moderation\Functional;
 | 
			
		||||
 | 
			
		||||
use Drupal\Tests\BrowserTestBase;
 | 
			
		||||
use Drupal\Tests\content_moderation\Traits\ContentModerationTestTrait;
 | 
			
		||||
use Drupal\Tests\node\Traits\ContentTypeCreationTrait;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Test revision revert.
 | 
			
		||||
 *
 | 
			
		||||
 * @group content_moderation
 | 
			
		||||
 */
 | 
			
		||||
class ModerationRevisionRevertTest extends BrowserTestBase {
 | 
			
		||||
 | 
			
		||||
  use ContentTypeCreationTrait;
 | 
			
		||||
  use ContentModerationTestTrait;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = [
 | 
			
		||||
    'content_moderation',
 | 
			
		||||
    'node',
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
 | 
			
		||||
    $moderated_bundle = $this->createContentType(['type' => 'moderated_bundle']);
 | 
			
		||||
    $moderated_bundle->save();
 | 
			
		||||
 | 
			
		||||
    $workflow = $this->createEditorialWorkflow();
 | 
			
		||||
    $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'moderated_bundle');
 | 
			
		||||
    $workflow->save();
 | 
			
		||||
 | 
			
		||||
    /** @var \Drupal\Core\Routing\RouteBuilderInterface $router_builder */
 | 
			
		||||
    $router_builder = $this->container->get('router.builder');
 | 
			
		||||
    $router_builder->rebuildIfNeeded();
 | 
			
		||||
 | 
			
		||||
    $admin = $this->drupalCreateUser([
 | 
			
		||||
      'access content overview',
 | 
			
		||||
      'administer nodes',
 | 
			
		||||
      'bypass node access',
 | 
			
		||||
      'view all revisions',
 | 
			
		||||
      'use editorial transition create_new_draft',
 | 
			
		||||
      'use editorial transition publish',
 | 
			
		||||
    ]);
 | 
			
		||||
    $this->drupalLogin($admin);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests that reverting a revision works.
 | 
			
		||||
   */
 | 
			
		||||
  public function testEditingAfterRevertRevision(): void {
 | 
			
		||||
    // Create a draft.
 | 
			
		||||
    $this->drupalGet('node/add/moderated_bundle');
 | 
			
		||||
    $this->submitForm([
 | 
			
		||||
      'title[0][value]' => 'First draft node',
 | 
			
		||||
      'moderation_state[0][state]' => 'draft',
 | 
			
		||||
    ], 'Save');
 | 
			
		||||
 | 
			
		||||
    // Now make it published.
 | 
			
		||||
    $this->drupalGet('node/1/edit');
 | 
			
		||||
    $this->submitForm([
 | 
			
		||||
      'title[0][value]' => 'Published node',
 | 
			
		||||
      'moderation_state[0][state]' => 'published',
 | 
			
		||||
    ], 'Save');
 | 
			
		||||
 | 
			
		||||
    // Check the editing form that show the published title.
 | 
			
		||||
    $this->drupalGet('node/1/edit');
 | 
			
		||||
    $this->assertSession()
 | 
			
		||||
      ->pageTextContains('Published node');
 | 
			
		||||
 | 
			
		||||
    // Revert the first revision.
 | 
			
		||||
    $revision_url = 'node/1/revisions/1/revert';
 | 
			
		||||
    $this->drupalGet($revision_url);
 | 
			
		||||
    $this->assertSession()->elementExists('css', '.form-submit');
 | 
			
		||||
    $this->click('.form-submit');
 | 
			
		||||
 | 
			
		||||
    // Check that it reverted.
 | 
			
		||||
    $this->drupalGet('node/1/edit');
 | 
			
		||||
    $this->assertSession()
 | 
			
		||||
      ->pageTextContains('First draft node');
 | 
			
		||||
    // Try to save the node.
 | 
			
		||||
    $this->drupalGet('node/1/edit');
 | 
			
		||||
    $this->submitForm(['moderation_state[0][state]' => 'draft'], 'Save');
 | 
			
		||||
 | 
			
		||||
    // Check if the submission passed the EntityChangedConstraintValidator.
 | 
			
		||||
    $this->assertSession()
 | 
			
		||||
      ->pageTextNotContains('The content has either been modified by another user, or you have already submitted modifications. As a result, your changes cannot be saved.');
 | 
			
		||||
 | 
			
		||||
    // Check the node has been saved.
 | 
			
		||||
    $this->assertSession()
 | 
			
		||||
      ->pageTextContains('moderated_bundle First draft node has been updated');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,108 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\content_moderation\Functional;
 | 
			
		||||
 | 
			
		||||
use Drupal\node\Entity\Node;
 | 
			
		||||
use Drupal\node\Entity\NodeType;
 | 
			
		||||
use Drupal\Tests\BrowserTestBase;
 | 
			
		||||
use Drupal\Tests\content_moderation\Traits\ContentModerationTestTrait;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests the view access control handler for moderation state entities.
 | 
			
		||||
 *
 | 
			
		||||
 * @group content_moderation
 | 
			
		||||
 */
 | 
			
		||||
class ModerationStateAccessTest extends BrowserTestBase {
 | 
			
		||||
 | 
			
		||||
  use ContentModerationTestTrait;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = [
 | 
			
		||||
    'content_moderation',
 | 
			
		||||
    'node',
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
 | 
			
		||||
    $node_type = NodeType::create([
 | 
			
		||||
      'type' => 'test',
 | 
			
		||||
      'name' => 'Test',
 | 
			
		||||
    ]);
 | 
			
		||||
    $node_type->save();
 | 
			
		||||
 | 
			
		||||
    $workflow = $this->createEditorialWorkflow();
 | 
			
		||||
    $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'test');
 | 
			
		||||
    $workflow->save();
 | 
			
		||||
 | 
			
		||||
    $this->container->get('module_installer')->install(['content_moderation_test_views']);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the view operation access handler with the view permission.
 | 
			
		||||
   */
 | 
			
		||||
  public function testViewShowsCorrectStates(): void {
 | 
			
		||||
    $permissions = [
 | 
			
		||||
      'access content',
 | 
			
		||||
      'view all revisions',
 | 
			
		||||
    ];
 | 
			
		||||
    $editor1 = $this->drupalCreateUser($permissions);
 | 
			
		||||
    $this->drupalLogin($editor1);
 | 
			
		||||
 | 
			
		||||
    $node_1 = Node::create([
 | 
			
		||||
      'type' => 'test',
 | 
			
		||||
      'title' => 'Draft node',
 | 
			
		||||
      'uid' => $editor1->id(),
 | 
			
		||||
    ]);
 | 
			
		||||
    $node_1->moderation_state->value = 'draft';
 | 
			
		||||
    $node_1->save();
 | 
			
		||||
 | 
			
		||||
    $node_2 = Node::create([
 | 
			
		||||
      'type' => 'test',
 | 
			
		||||
      'title' => 'Published node',
 | 
			
		||||
      'uid' => $editor1->id(),
 | 
			
		||||
    ]);
 | 
			
		||||
    $node_2->moderation_state->value = 'published';
 | 
			
		||||
    $node_2->save();
 | 
			
		||||
 | 
			
		||||
    // Resave the node with a new state.
 | 
			
		||||
    $node_2->setTitle('Archived node');
 | 
			
		||||
    $node_2->moderation_state->value = 'archived';
 | 
			
		||||
    $node_2->save();
 | 
			
		||||
 | 
			
		||||
    // Now show the View, and confirm that the state labels are showing.
 | 
			
		||||
    $this->drupalGet('/latest');
 | 
			
		||||
    $page = $this->getSession()->getPage();
 | 
			
		||||
    $this->assertTrue($page->hasContent('Draft'));
 | 
			
		||||
    $this->assertTrue($page->hasContent('Archived'));
 | 
			
		||||
    $this->assertFalse($page->hasContent('Published'));
 | 
			
		||||
 | 
			
		||||
    // Now log in as an admin and test the same thing.
 | 
			
		||||
    $permissions = [
 | 
			
		||||
      'access content',
 | 
			
		||||
      'view all revisions',
 | 
			
		||||
    ];
 | 
			
		||||
    $admin1 = $this->drupalCreateUser($permissions);
 | 
			
		||||
    $this->drupalLogin($admin1);
 | 
			
		||||
 | 
			
		||||
    $this->drupalGet('/latest');
 | 
			
		||||
    $page = $this->getSession()->getPage();
 | 
			
		||||
    $this->assertEquals(200, $this->getSession()->getStatusCode());
 | 
			
		||||
    $this->assertTrue($page->hasContent('Draft'));
 | 
			
		||||
    $this->assertTrue($page->hasContent('Archived'));
 | 
			
		||||
    $this->assertFalse($page->hasContent('Published'));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,160 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\content_moderation\Functional;
 | 
			
		||||
 | 
			
		||||
use Drupal\block_content\Entity\BlockContent;
 | 
			
		||||
use Drupal\block_content\Entity\BlockContentType;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests general content moderation workflow for blocks.
 | 
			
		||||
 *
 | 
			
		||||
 * @group content_moderation
 | 
			
		||||
 */
 | 
			
		||||
class ModerationStateBlockTest extends ModerationStateTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function getAdministratorPermissions(): array {
 | 
			
		||||
    return array_merge($this->permissions, [
 | 
			
		||||
      'administer blocks',
 | 
			
		||||
      'administer block content',
 | 
			
		||||
    ]);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
 | 
			
		||||
    // Create the "basic" block type.
 | 
			
		||||
    $bundle = BlockContentType::create([
 | 
			
		||||
      'id' => 'basic',
 | 
			
		||||
      'label' => 'basic',
 | 
			
		||||
      'revision' => FALSE,
 | 
			
		||||
    ]);
 | 
			
		||||
    $bundle->save();
 | 
			
		||||
 | 
			
		||||
    // Add the body field to it.
 | 
			
		||||
    block_content_add_body_field($bundle->id());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests moderating content blocks.
 | 
			
		||||
   *
 | 
			
		||||
   * Blocks and any non-node-type-entities do not have a concept of
 | 
			
		||||
   * "published". As such, we must use the "default revision" to know what is
 | 
			
		||||
   * going to be "published", i.e. visible to the user.
 | 
			
		||||
   *
 | 
			
		||||
   * The one exception is a block that has never been "published". When a block
 | 
			
		||||
   * is first created, it becomes the "default revision". For each edit of the
 | 
			
		||||
   * block after that, Content Moderation checks the "default revision" to
 | 
			
		||||
   * see if it is set to a published moderation state. If it is not, the entity
 | 
			
		||||
   * being saved will become the "default revision".
 | 
			
		||||
   *
 | 
			
		||||
   * The test below is intended, in part, to make this behavior clear.
 | 
			
		||||
   *
 | 
			
		||||
   * @see \Drupal\content_moderation\EntityOperations::entityPresave
 | 
			
		||||
   * @see \Drupal\content_moderation\Tests\ModerationFormTest::testModerationForm
 | 
			
		||||
   */
 | 
			
		||||
  public function testCustomBlockModeration(): void {
 | 
			
		||||
    $this->adminUser = $this->drupalCreateUser($this->getAdministratorPermissions());
 | 
			
		||||
    $this->drupalLogin($this->adminUser);
 | 
			
		||||
 | 
			
		||||
    // Enable moderation for content blocks.
 | 
			
		||||
    $edit['bundles[basic]'] = TRUE;
 | 
			
		||||
    $this->drupalGet('admin/config/workflow/workflows/manage/editorial/type/block_content');
 | 
			
		||||
    $this->submitForm($edit, 'Save');
 | 
			
		||||
 | 
			
		||||
    // Create a content block at block/add and save it as draft.
 | 
			
		||||
    $body = 'Body of moderated block';
 | 
			
		||||
    $edit = [
 | 
			
		||||
      'info[0][value]' => 'Moderated block',
 | 
			
		||||
      'moderation_state[0][state]' => 'draft',
 | 
			
		||||
      'body[0][value]' => $body,
 | 
			
		||||
    ];
 | 
			
		||||
    $this->drupalGet('block/add');
 | 
			
		||||
    $this->submitForm($edit, 'Save');
 | 
			
		||||
    $this->assertSession()->pageTextContains('basic Moderated block has been created.');
 | 
			
		||||
 | 
			
		||||
    // Place the block in the Sidebar First region.
 | 
			
		||||
    $instance = [
 | 
			
		||||
      'id' => 'moderated_block',
 | 
			
		||||
      'settings[label]' => $edit['info[0][value]'],
 | 
			
		||||
      'region' => 'sidebar_first',
 | 
			
		||||
    ];
 | 
			
		||||
    $block = BlockContent::load(1);
 | 
			
		||||
    $url = 'admin/structure/block/add/block_content:' . $block->uuid() . '/' . $this->config('system.theme')->get('default');
 | 
			
		||||
    $this->drupalGet($url);
 | 
			
		||||
    $this->submitForm($instance, 'Save block');
 | 
			
		||||
 | 
			
		||||
    // Navigate to home page and check that the block is visible. It should be
 | 
			
		||||
    // visible because it is the default revision.
 | 
			
		||||
    $this->drupalGet('');
 | 
			
		||||
    $this->assertSession()->pageTextContains($body);
 | 
			
		||||
 | 
			
		||||
    // Update the block.
 | 
			
		||||
    $updated_body = 'This is the new body value';
 | 
			
		||||
    $edit = [
 | 
			
		||||
      'body[0][value]' => $updated_body,
 | 
			
		||||
      'moderation_state[0][state]' => 'draft',
 | 
			
		||||
    ];
 | 
			
		||||
    $this->drupalGet('admin/content/block/' . $block->id());
 | 
			
		||||
    $this->submitForm($edit, 'Save');
 | 
			
		||||
    $this->assertSession()->pageTextContains('basic Moderated block has been updated.');
 | 
			
		||||
 | 
			
		||||
    // Navigate to the home page and check that the block shows the updated
 | 
			
		||||
    // content. It should show the updated content because the block's default
 | 
			
		||||
    // revision is not a published moderation state.
 | 
			
		||||
    $this->drupalGet('');
 | 
			
		||||
    $this->assertSession()->pageTextContains($updated_body);
 | 
			
		||||
 | 
			
		||||
    // Publish the block so we can create a pending revision.
 | 
			
		||||
    $this->drupalGet('admin/content/block/' . $block->id());
 | 
			
		||||
    $this->submitForm(['moderation_state[0][state]' => 'published'], 'Save');
 | 
			
		||||
 | 
			
		||||
    // Create a pending revision.
 | 
			
		||||
    $pending_revision_body = 'This is the pending revision body value';
 | 
			
		||||
    $edit = [
 | 
			
		||||
      'body[0][value]' => $pending_revision_body,
 | 
			
		||||
      'moderation_state[0][state]' => 'draft',
 | 
			
		||||
    ];
 | 
			
		||||
    $this->drupalGet('admin/content/block/' . $block->id());
 | 
			
		||||
    $this->submitForm($edit, 'Save');
 | 
			
		||||
    $this->assertSession()->pageTextContains('basic Moderated block has been updated.');
 | 
			
		||||
 | 
			
		||||
    // Navigate to home page and check that the pending revision doesn't show,
 | 
			
		||||
    // since it should not be set as the default revision.
 | 
			
		||||
    $this->drupalGet('');
 | 
			
		||||
    $this->assertSession()->pageTextContains($updated_body);
 | 
			
		||||
 | 
			
		||||
    // Open the latest tab and publish the new draft.
 | 
			
		||||
    $edit = [
 | 
			
		||||
      'new_state' => 'published',
 | 
			
		||||
    ];
 | 
			
		||||
    $this->drupalGet('admin/content/block/' . $block->id() . '/latest');
 | 
			
		||||
    $this->submitForm($edit, 'Apply');
 | 
			
		||||
    $this->assertSession()->pageTextContains('The moderation state has been updated.');
 | 
			
		||||
 | 
			
		||||
    // Navigate to home page and check that the pending revision is now the
 | 
			
		||||
    // default revision and therefore visible.
 | 
			
		||||
    $this->drupalGet('');
 | 
			
		||||
    $this->assertSession()->pageTextContains($pending_revision_body);
 | 
			
		||||
 | 
			
		||||
    // Check that revision is checked by default when content moderation is
 | 
			
		||||
    // enabled.
 | 
			
		||||
    $this->drupalGet('/admin/content/block/' . $block->id());
 | 
			
		||||
    $this->assertSession()->checkboxChecked('revision');
 | 
			
		||||
    $this->assertSession()->pageTextContains('Revisions must be required when moderation is enabled.');
 | 
			
		||||
    $this->assertSession()->fieldDisabled('revision');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,183 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\content_moderation\Functional;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Url;
 | 
			
		||||
use Drupal\node\Entity\Node;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests general content moderation workflow for nodes.
 | 
			
		||||
 *
 | 
			
		||||
 * @group content_moderation
 | 
			
		||||
 */
 | 
			
		||||
class ModerationStateNodeTest extends ModerationStateTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
    $this->drupalLogin($this->adminUser);
 | 
			
		||||
    $this->createContentTypeFromUi('Moderated content', 'moderated_content', TRUE);
 | 
			
		||||
    $this->grantUserPermissionToCreateContentOfType($this->adminUser, 'moderated_content');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests creating and deleting content.
 | 
			
		||||
   */
 | 
			
		||||
  public function testCreatingContent(): void {
 | 
			
		||||
    $this->drupalGet('node/add/moderated_content');
 | 
			
		||||
    $this->submitForm([
 | 
			
		||||
      'title[0][value]' => 'moderated content',
 | 
			
		||||
      'moderation_state[0][state]' => 'draft',
 | 
			
		||||
    ], 'Save');
 | 
			
		||||
    $node = $this->getNodeByTitle('moderated content');
 | 
			
		||||
    if (!$node) {
 | 
			
		||||
      $this->fail('Test node was not saved correctly.');
 | 
			
		||||
    }
 | 
			
		||||
    $this->assertEquals('draft', $node->moderation_state->value);
 | 
			
		||||
 | 
			
		||||
    $path = 'node/' . $node->id() . '/edit';
 | 
			
		||||
    // Set up published revision.
 | 
			
		||||
    $this->drupalGet($path);
 | 
			
		||||
    $this->submitForm(['moderation_state[0][state]' => 'published'], 'Save');
 | 
			
		||||
    /** @var \Drupal\node\NodeInterface $node */
 | 
			
		||||
    $node = \Drupal::entityTypeManager()->getStorage('node')->load($node->id());
 | 
			
		||||
    $this->assertTrue($node->isPublished());
 | 
			
		||||
    $this->assertEquals('published', $node->moderation_state->value);
 | 
			
		||||
 | 
			
		||||
    // Verify that the state field is not shown.
 | 
			
		||||
    $this->assertSession()->pageTextNotContains('Published');
 | 
			
		||||
 | 
			
		||||
    // Delete the node.
 | 
			
		||||
    $this->drupalGet('node/' . $node->id() . '/delete');
 | 
			
		||||
    $this->submitForm([], 'Delete');
 | 
			
		||||
    $this->assertSession()->pageTextContains('The Moderated content moderated content has been deleted.');
 | 
			
		||||
 | 
			
		||||
    // Disable content moderation.
 | 
			
		||||
    $edit['bundles[moderated_content]'] = FALSE;
 | 
			
		||||
    $this->drupalGet('admin/config/workflow/workflows/manage/editorial/type/node');
 | 
			
		||||
    $this->submitForm($edit, 'Save');
 | 
			
		||||
    // Ensure the parent environment is up-to-date.
 | 
			
		||||
    // @see content_moderation_workflow_insert()
 | 
			
		||||
    \Drupal::service('entity_type.bundle.info')->clearCachedBundles();
 | 
			
		||||
    \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
 | 
			
		||||
 | 
			
		||||
    // Create a new node.
 | 
			
		||||
    $this->drupalGet('node/add/moderated_content');
 | 
			
		||||
    $this->submitForm(['title[0][value]' => 'non-moderated content'], 'Save');
 | 
			
		||||
 | 
			
		||||
    $node = $this->getNodeByTitle('non-moderated content');
 | 
			
		||||
    if (!$node) {
 | 
			
		||||
      $this->fail('Non-moderated test node was not saved correctly.');
 | 
			
		||||
    }
 | 
			
		||||
    $this->assertFalse($node->hasField('moderation_state'));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests edit form destinations.
 | 
			
		||||
   */
 | 
			
		||||
  public function testFormSaveDestination(): void {
 | 
			
		||||
    // Create new moderated content in draft.
 | 
			
		||||
    $this->drupalGet('node/add/moderated_content');
 | 
			
		||||
    $this->submitForm([
 | 
			
		||||
      'title[0][value]' => 'Some moderated content',
 | 
			
		||||
      'body[0][value]' => 'First version of the content.',
 | 
			
		||||
      'moderation_state[0][state]' => 'draft',
 | 
			
		||||
    ], 'Save');
 | 
			
		||||
 | 
			
		||||
    $node = $this->drupalGetNodeByTitle('Some moderated content');
 | 
			
		||||
    $edit_path = sprintf('node/%d/edit', $node->id());
 | 
			
		||||
 | 
			
		||||
    // After saving, we should be at the canonical URL and viewing the first
 | 
			
		||||
    // revision.
 | 
			
		||||
    $this->assertSession()->addressEquals(Url::fromRoute('entity.node.canonical', ['node' => $node->id()]));
 | 
			
		||||
    $this->assertSession()->pageTextContains('First version of the content.');
 | 
			
		||||
 | 
			
		||||
    // Create a new draft; after saving, we should still be on the canonical
 | 
			
		||||
    // URL, but viewing the second revision.
 | 
			
		||||
    $this->drupalGet($edit_path);
 | 
			
		||||
    $this->submitForm([
 | 
			
		||||
      'body[0][value]' => 'Second version of the content.',
 | 
			
		||||
      'moderation_state[0][state]' => 'draft',
 | 
			
		||||
    ], 'Save');
 | 
			
		||||
    $this->assertSession()->addressEquals(Url::fromRoute('entity.node.canonical', ['node' => $node->id()]));
 | 
			
		||||
    $this->assertSession()->pageTextContains('Second version of the content.');
 | 
			
		||||
 | 
			
		||||
    // Make a new published revision; after saving, we should be at the
 | 
			
		||||
    // canonical URL.
 | 
			
		||||
    $this->drupalGet($edit_path);
 | 
			
		||||
    $this->submitForm([
 | 
			
		||||
      'body[0][value]' => 'Third version of the content.',
 | 
			
		||||
      'moderation_state[0][state]' => 'published',
 | 
			
		||||
    ], 'Save');
 | 
			
		||||
    $this->assertSession()->addressEquals(Url::fromRoute('entity.node.canonical', ['node' => $node->id()]));
 | 
			
		||||
    $this->assertSession()->pageTextContains('Third version of the content.');
 | 
			
		||||
 | 
			
		||||
    // Make a new pending revision; after saving, we should be on the "Latest
 | 
			
		||||
    // version" tab.
 | 
			
		||||
    $this->drupalGet($edit_path);
 | 
			
		||||
    $this->submitForm([
 | 
			
		||||
      'body[0][value]' => 'Fourth version of the content.',
 | 
			
		||||
      'moderation_state[0][state]' => 'draft',
 | 
			
		||||
    ], 'Save');
 | 
			
		||||
    $this->assertSession()->addressEquals(Url::fromRoute('entity.node.latest_version', ['node' => $node->id()]));
 | 
			
		||||
    $this->assertSession()->pageTextContains('Fourth version of the content.');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests pagers aren't broken by content_moderation.
 | 
			
		||||
   */
 | 
			
		||||
  public function testPagers(): void {
 | 
			
		||||
    // Create 51 nodes to force the pager.
 | 
			
		||||
    foreach (range(1, 51) as $delta) {
 | 
			
		||||
      Node::create([
 | 
			
		||||
        'type' => 'moderated_content',
 | 
			
		||||
        'uid' => $this->adminUser->id(),
 | 
			
		||||
        'title' => 'Node ' . $delta,
 | 
			
		||||
        'status' => 1,
 | 
			
		||||
        'moderation_state' => 'published',
 | 
			
		||||
      ])->save();
 | 
			
		||||
    }
 | 
			
		||||
    $this->drupalLogin($this->adminUser);
 | 
			
		||||
    $this->drupalGet('admin/content');
 | 
			
		||||
    $element = $this->cssSelect('nav.pager li.is-active a');
 | 
			
		||||
    $url = $element[0]->getAttribute('href');
 | 
			
		||||
    $query = [];
 | 
			
		||||
    parse_str(parse_url($url, PHP_URL_QUERY), $query);
 | 
			
		||||
    $this->assertEquals(0, $query['page']);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the workflow when a user has no Content Moderation permissions.
 | 
			
		||||
   */
 | 
			
		||||
  public function testNoContentModerationPermissions(): void {
 | 
			
		||||
    $session_assert = $this->assertSession();
 | 
			
		||||
 | 
			
		||||
    // Create a user with quite advanced node permissions but no content
 | 
			
		||||
    // moderation permissions.
 | 
			
		||||
    $limited_user = $this->createUser([
 | 
			
		||||
      'administer nodes',
 | 
			
		||||
      'bypass node access',
 | 
			
		||||
    ]);
 | 
			
		||||
    $this->drupalLogin($limited_user);
 | 
			
		||||
 | 
			
		||||
    // Check the user can see the content entity form, but can't see the
 | 
			
		||||
    // moderation state select or save the entity form.
 | 
			
		||||
    $this->drupalGet('node/add/moderated_content');
 | 
			
		||||
    $session_assert->statusCodeEquals(200);
 | 
			
		||||
    $session_assert->fieldNotExists('moderation_state[0][state]');
 | 
			
		||||
    $this->submitForm([
 | 
			
		||||
      'title[0][value]' => 'moderated content',
 | 
			
		||||
    ], 'Save');
 | 
			
		||||
    $session_assert->pageTextContains('You do not have access to transition from Draft to Draft');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,122 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\content_moderation\Functional;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests moderation state node type integration.
 | 
			
		||||
 *
 | 
			
		||||
 * @group content_moderation
 | 
			
		||||
 */
 | 
			
		||||
class ModerationStateNodeTypeTest extends ModerationStateTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * A node type without moderation state disabled.
 | 
			
		||||
   *
 | 
			
		||||
   * @covers \Drupal\content_moderation\EntityTypeInfo::formAlter
 | 
			
		||||
   * @covers \Drupal\content_moderation\Entity\Handler\NodeModerationHandler::enforceRevisionsBundleFormAlter
 | 
			
		||||
   */
 | 
			
		||||
  public function testNotModerated(): void {
 | 
			
		||||
    $this->drupalLogin($this->adminUser);
 | 
			
		||||
    $this->createContentTypeFromUi('Not moderated', 'not_moderated');
 | 
			
		||||
    $this->assertSession()->pageTextContains('The content type Not moderated has been added.');
 | 
			
		||||
    $this->grantUserPermissionToCreateContentOfType($this->adminUser, 'not_moderated');
 | 
			
		||||
    $this->drupalGet('node/add/not_moderated');
 | 
			
		||||
    $this->assertSession()->pageTextContains('Save');
 | 
			
		||||
    $this->submitForm([
 | 
			
		||||
      'title[0][value]' => 'Test',
 | 
			
		||||
    ], 'Save');
 | 
			
		||||
    $this->assertSession()->pageTextContains('Not moderated Test has been created.');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests enabling moderation on an existing node-type, with content.
 | 
			
		||||
   *
 | 
			
		||||
   * @covers \Drupal\content_moderation\EntityTypeInfo::formAlter
 | 
			
		||||
   * @covers \Drupal\content_moderation\Entity\Handler\NodeModerationHandler::enforceRevisionsBundleFormAlter
 | 
			
		||||
   */
 | 
			
		||||
  public function testEnablingOnExistingContent(): void {
 | 
			
		||||
    $editor_permissions = [
 | 
			
		||||
      'administer workflows',
 | 
			
		||||
      'access administration pages',
 | 
			
		||||
      'administer content types',
 | 
			
		||||
      'administer nodes',
 | 
			
		||||
      'view latest version',
 | 
			
		||||
      'view any unpublished content',
 | 
			
		||||
      'access content overview',
 | 
			
		||||
      'use editorial transition create_new_draft',
 | 
			
		||||
    ];
 | 
			
		||||
    $publish_permissions = array_merge($editor_permissions, ['use editorial transition publish']);
 | 
			
		||||
    $editor = $this->drupalCreateUser($editor_permissions);
 | 
			
		||||
    $editor_with_publish = $this->drupalCreateUser($publish_permissions);
 | 
			
		||||
 | 
			
		||||
    // Create a node type that is not moderated.
 | 
			
		||||
    $this->drupalLogin($editor);
 | 
			
		||||
    $this->createContentTypeFromUi('Not moderated', 'not_moderated');
 | 
			
		||||
    $this->grantUserPermissionToCreateContentOfType($editor, 'not_moderated');
 | 
			
		||||
    $this->grantUserPermissionToCreateContentOfType($editor_with_publish, 'not_moderated');
 | 
			
		||||
 | 
			
		||||
    // Create content.
 | 
			
		||||
    $this->drupalGet('node/add/not_moderated');
 | 
			
		||||
    $this->submitForm([
 | 
			
		||||
      'title[0][value]' => 'Test',
 | 
			
		||||
    ], 'Save');
 | 
			
		||||
    $this->assertSession()->pageTextContains('Not moderated Test has been created.');
 | 
			
		||||
 | 
			
		||||
    // Check that the 'Create new revision' is not disabled.
 | 
			
		||||
    $this->drupalGet('/admin/structure/types/manage/not_moderated');
 | 
			
		||||
    $this->assertNull($this->assertSession()->fieldExists('options[revision]')->getAttribute('disabled'));
 | 
			
		||||
 | 
			
		||||
    // Now enable moderation state.
 | 
			
		||||
    $this->enableModerationThroughUi('not_moderated');
 | 
			
		||||
 | 
			
		||||
    // Check that the 'Create new revision' checkbox is checked and disabled.
 | 
			
		||||
    $this->drupalGet('/admin/structure/types/manage/not_moderated');
 | 
			
		||||
    $this->assertSession()->checkboxChecked('options[revision]');
 | 
			
		||||
    $this->assertSession()->fieldDisabled('options[revision]');
 | 
			
		||||
 | 
			
		||||
    // And make sure it works.
 | 
			
		||||
    $nodes = \Drupal::entityTypeManager()->getStorage('node')
 | 
			
		||||
      ->loadByProperties(['title' => 'Test']);
 | 
			
		||||
    if (empty($nodes)) {
 | 
			
		||||
      $this->fail('Could not load node with title Test');
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    $node = reset($nodes);
 | 
			
		||||
    $this->drupalGet('node/' . $node->id());
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
    $this->assertSession()->linkByHrefExists('node/' . $node->id() . '/edit');
 | 
			
		||||
    $this->drupalGet('node/' . $node->id() . '/edit');
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
    $this->assertSession()->optionExists('moderation_state[0][state]', 'draft');
 | 
			
		||||
    $this->assertSession()->optionNotExists('moderation_state[0][state]', 'published');
 | 
			
		||||
 | 
			
		||||
    $this->drupalLogin($editor_with_publish);
 | 
			
		||||
    $this->drupalGet('node/' . $node->id() . '/edit');
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
    $this->assertSession()->optionExists('moderation_state[0][state]', 'draft');
 | 
			
		||||
    $this->assertSession()->optionExists('moderation_state[0][state]', 'published');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @covers \Drupal\content_moderation\Entity\Handler\NodeModerationHandler::enforceRevisionsBundleFormAlter
 | 
			
		||||
   */
 | 
			
		||||
  public function testEnforceRevisionsEntityFormAlter(): void {
 | 
			
		||||
    $this->drupalLogin($this->adminUser);
 | 
			
		||||
    $this->createContentTypeFromUi('Moderated', 'moderated');
 | 
			
		||||
 | 
			
		||||
    // Ensure checkboxes in the 'workflow' section can be altered, even when
 | 
			
		||||
    // 'revision' is enforced and disabled.
 | 
			
		||||
    $this->drupalGet('admin/structure/types/manage/moderated');
 | 
			
		||||
    $this->submitForm(['options[promote]' => TRUE], 'Save');
 | 
			
		||||
    $this->drupalGet('admin/structure/types/manage/moderated');
 | 
			
		||||
    $this->assertSession()->checkboxChecked('options[promote]');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,74 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\content_moderation\Functional;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Language\LanguageInterface;
 | 
			
		||||
use Drupal\taxonomy\Entity\Term;
 | 
			
		||||
use Drupal\taxonomy\Entity\Vocabulary;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests the taxonomy term moderation handler.
 | 
			
		||||
 *
 | 
			
		||||
 * @group content_moderation
 | 
			
		||||
 */
 | 
			
		||||
class ModerationStateTaxonomyTermTest extends ModerationStateTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
 | 
			
		||||
    // Create a "Tags" vocabulary.
 | 
			
		||||
    Vocabulary::create([
 | 
			
		||||
      'vid' => 'tags',
 | 
			
		||||
      'name' => 'Tags',
 | 
			
		||||
      'new_revision' => FALSE,
 | 
			
		||||
    ])->save();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the taxonomy term moderation handler alters the forms as intended.
 | 
			
		||||
   *
 | 
			
		||||
   * @covers \Drupal\content_moderation\Entity\Handler\TaxonomyTermModerationHandler::enforceRevisionsEntityFormAlter
 | 
			
		||||
   * @covers \Drupal\content_moderation\Entity\Handler\TaxonomyTermModerationHandler::enforceRevisionsBundleFormAlter
 | 
			
		||||
   */
 | 
			
		||||
  public function testEnforceRevisionsEntityFormAlter(): void {
 | 
			
		||||
    $this->drupalLogin($this->adminUser);
 | 
			
		||||
 | 
			
		||||
    // Enable moderation for the tags vocabulary.
 | 
			
		||||
    $edit['bundles[tags]'] = TRUE;
 | 
			
		||||
    $this->drupalGet('/admin/config/workflow/workflows/manage/editorial/type/taxonomy_term');
 | 
			
		||||
    $this->submitForm($edit, 'Save');
 | 
			
		||||
 | 
			
		||||
    // Check that revision is checked by default when content moderation is
 | 
			
		||||
    // enabled for the vocabulary.
 | 
			
		||||
    $this->drupalGet('/admin/structure/taxonomy/manage/tags');
 | 
			
		||||
    $this->assertSession()->checkboxChecked('revision');
 | 
			
		||||
    $this->assertSession()->pageTextContains('Revisions must be required when moderation is enabled.');
 | 
			
		||||
    $this->assertSession()->fieldDisabled('revision');
 | 
			
		||||
 | 
			
		||||
    // Create a taxonomy term and save it as draft.
 | 
			
		||||
    $term = Term::create([
 | 
			
		||||
      'name' => 'Test tag',
 | 
			
		||||
      'vid' => 'tags',
 | 
			
		||||
      'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
 | 
			
		||||
    ]);
 | 
			
		||||
    $term->save();
 | 
			
		||||
 | 
			
		||||
    // Check that revision is checked by default when editing a term and
 | 
			
		||||
    // content moderation is enabled for the term's vocabulary.
 | 
			
		||||
    $this->drupalGet($term->toUrl('edit-form'));
 | 
			
		||||
    $this->assertSession()->checkboxChecked('revision');
 | 
			
		||||
    $this->assertSession()->pageTextContains('Revisions must be required when moderation is enabled.');
 | 
			
		||||
    $this->assertSession()->fieldDisabled('revision');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,173 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\content_moderation\Functional;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Session\AccountInterface;
 | 
			
		||||
use Drupal\node\Entity\NodeType;
 | 
			
		||||
use Drupal\Tests\BrowserTestBase;
 | 
			
		||||
use Drupal\Tests\content_moderation\Traits\ContentModerationTestTrait;
 | 
			
		||||
use Drupal\user\Entity\Role;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Defines a base class for moderation state tests.
 | 
			
		||||
 */
 | 
			
		||||
abstract class ModerationStateTestBase extends BrowserTestBase {
 | 
			
		||||
 | 
			
		||||
  use ContentModerationTestTrait;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Profile to use.
 | 
			
		||||
   *
 | 
			
		||||
   * @var string
 | 
			
		||||
   */
 | 
			
		||||
  protected $profile = 'testing';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Admin user.
 | 
			
		||||
   *
 | 
			
		||||
   * @var \Drupal\Core\Session\AccountInterface
 | 
			
		||||
   */
 | 
			
		||||
  protected $adminUser;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Permissions to grant admin user.
 | 
			
		||||
   *
 | 
			
		||||
   * @var array
 | 
			
		||||
   */
 | 
			
		||||
  protected $permissions = [
 | 
			
		||||
    'administer workflows',
 | 
			
		||||
    'access administration pages',
 | 
			
		||||
    'administer content types',
 | 
			
		||||
    'administer nodes',
 | 
			
		||||
    'view latest version',
 | 
			
		||||
    'view any unpublished content',
 | 
			
		||||
    'access content overview',
 | 
			
		||||
    'use editorial transition create_new_draft',
 | 
			
		||||
    'use editorial transition publish',
 | 
			
		||||
    'use editorial transition archive',
 | 
			
		||||
    'use editorial transition archived_draft',
 | 
			
		||||
    'use editorial transition archived_published',
 | 
			
		||||
    'administer taxonomy',
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The editorial workflow entity.
 | 
			
		||||
   *
 | 
			
		||||
   * @var \Drupal\workflows\Entity\Workflow
 | 
			
		||||
   */
 | 
			
		||||
  protected $workflow;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = [
 | 
			
		||||
    'content_moderation',
 | 
			
		||||
    'block',
 | 
			
		||||
    'block_content',
 | 
			
		||||
    'node',
 | 
			
		||||
    'entity_test',
 | 
			
		||||
    'taxonomy',
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Sets the test up.
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
    $this->workflow = $this->createEditorialWorkflow();
 | 
			
		||||
    $this->adminUser = $this->drupalCreateUser($this->permissions);
 | 
			
		||||
    $this->drupalPlaceBlock('local_tasks_block', ['id' => 'tabs_block']);
 | 
			
		||||
    $this->drupalPlaceBlock('page_title_block');
 | 
			
		||||
    $this->drupalPlaceBlock('local_actions_block', ['id' => 'actions_block']);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Gets the permission machine name for a transition.
 | 
			
		||||
   *
 | 
			
		||||
   * @param string $workflow_id
 | 
			
		||||
   *   The workflow ID.
 | 
			
		||||
   * @param string $transition_id
 | 
			
		||||
   *   The transition ID.
 | 
			
		||||
   *
 | 
			
		||||
   * @return string
 | 
			
		||||
   *   The permission machine name for a transition.
 | 
			
		||||
   */
 | 
			
		||||
  protected function getWorkflowTransitionPermission($workflow_id, $transition_id) {
 | 
			
		||||
    return 'use ' . $workflow_id . ' transition ' . $transition_id;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Creates a content-type from the UI.
 | 
			
		||||
   *
 | 
			
		||||
   * @param string $content_type_name
 | 
			
		||||
   *   Content type human name.
 | 
			
		||||
   * @param string $content_type_id
 | 
			
		||||
   *   Machine name.
 | 
			
		||||
   * @param bool $moderated
 | 
			
		||||
   *   TRUE if should be moderated.
 | 
			
		||||
   * @param string $workflow_id
 | 
			
		||||
   *   The workflow to attach to the bundle.
 | 
			
		||||
   */
 | 
			
		||||
  protected function createContentTypeFromUi($content_type_name, $content_type_id, $moderated = FALSE, $workflow_id = 'editorial') {
 | 
			
		||||
    $this->drupalGet('admin/structure/types');
 | 
			
		||||
    $this->clickLink('Add content type');
 | 
			
		||||
 | 
			
		||||
    $edit = [
 | 
			
		||||
      'name' => $content_type_name,
 | 
			
		||||
      'type' => $content_type_id,
 | 
			
		||||
    ];
 | 
			
		||||
    $this->submitForm($edit, 'Save');
 | 
			
		||||
 | 
			
		||||
    // Check the content type has been set to create new revisions.
 | 
			
		||||
    $this->assertTrue(NodeType::load($content_type_id)->shouldCreateNewRevision());
 | 
			
		||||
 | 
			
		||||
    if ($moderated) {
 | 
			
		||||
      $this->enableModerationThroughUi($content_type_id, $workflow_id);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Enable moderation for a specified content type, using the UI.
 | 
			
		||||
   *
 | 
			
		||||
   * @param string $content_type_id
 | 
			
		||||
   *   Machine name.
 | 
			
		||||
   * @param string $workflow_id
 | 
			
		||||
   *   The workflow to attach to the bundle.
 | 
			
		||||
   */
 | 
			
		||||
  public function enableModerationThroughUi($content_type_id, $workflow_id = 'editorial') {
 | 
			
		||||
    $this->drupalGet('/admin/config/workflow/workflows');
 | 
			
		||||
    $this->assertSession()->linkByHrefExists('admin/config/workflow/workflows/manage/' . $workflow_id);
 | 
			
		||||
    $edit['bundles[' . $content_type_id . ']'] = TRUE;
 | 
			
		||||
    $this->drupalGet('admin/config/workflow/workflows/manage/' . $workflow_id . '/type/node');
 | 
			
		||||
    $this->submitForm($edit, 'Save');
 | 
			
		||||
    // Ensure the parent environment is up-to-date.
 | 
			
		||||
    // @see content_moderation_workflow_insert()
 | 
			
		||||
    \Drupal::service('entity_type.bundle.info')->clearCachedBundles();
 | 
			
		||||
    \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
 | 
			
		||||
    /** @var \Drupal\Core\Routing\RouteBuilderInterface $router_builder */
 | 
			
		||||
    $router_builder = $this->container->get('router.builder');
 | 
			
		||||
    $router_builder->rebuildIfNeeded();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Grants given user permission to create content of given type.
 | 
			
		||||
   *
 | 
			
		||||
   * @param \Drupal\Core\Session\AccountInterface $account
 | 
			
		||||
   *   User to grant permission to.
 | 
			
		||||
   * @param string $content_type_id
 | 
			
		||||
   *   Content type ID.
 | 
			
		||||
   */
 | 
			
		||||
  protected function grantUserPermissionToCreateContentOfType(AccountInterface $account, $content_type_id) {
 | 
			
		||||
    $role_ids = $account->getRoles(TRUE);
 | 
			
		||||
    /** @var \Drupal\user\RoleInterface $role */
 | 
			
		||||
    $role_id = reset($role_ids);
 | 
			
		||||
    $role = Role::load($role_id);
 | 
			
		||||
    $role->grantPermission(sprintf('create %s content', $content_type_id));
 | 
			
		||||
    $role->grantPermission(sprintf('edit any %s content', $content_type_id));
 | 
			
		||||
    $role->grantPermission(sprintf('delete any %s content', $content_type_id));
 | 
			
		||||
    $role->save();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,198 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\content_moderation\Functional;
 | 
			
		||||
 | 
			
		||||
use Drupal\node\Entity\NodeType;
 | 
			
		||||
use Drupal\Tests\node\Traits\NodeAccessTrait;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests permission access control around nodes.
 | 
			
		||||
 *
 | 
			
		||||
 * @group content_moderation
 | 
			
		||||
 */
 | 
			
		||||
class NodeAccessTest extends ModerationStateTestBase {
 | 
			
		||||
 | 
			
		||||
  use NodeAccessTrait;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = [
 | 
			
		||||
    'content_moderation',
 | 
			
		||||
    'block',
 | 
			
		||||
    'block_content',
 | 
			
		||||
    'node',
 | 
			
		||||
    'node_access_test',
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Permissions to grant admin user.
 | 
			
		||||
   *
 | 
			
		||||
   * @var array
 | 
			
		||||
   */
 | 
			
		||||
  protected $permissions = [
 | 
			
		||||
    'administer workflows',
 | 
			
		||||
    'access administration pages',
 | 
			
		||||
    'administer content types',
 | 
			
		||||
    'administer nodes',
 | 
			
		||||
    'view latest version',
 | 
			
		||||
    'view any unpublished content',
 | 
			
		||||
    'access content overview',
 | 
			
		||||
    'use editorial transition create_new_draft',
 | 
			
		||||
    'use editorial transition publish',
 | 
			
		||||
    'bypass node access',
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
    $this->drupalLogin($this->adminUser);
 | 
			
		||||
    $this->createContentTypeFromUi('Moderated content', 'moderated_content', FALSE);
 | 
			
		||||
    // Ensure the statically cached entity bundle info is aware of the content
 | 
			
		||||
    // type that was just created in the UI.
 | 
			
		||||
    $this->container->get('entity_type.bundle.info')->clearCachedBundles();
 | 
			
		||||
    $this->grantUserPermissionToCreateContentOfType($this->adminUser, 'moderated_content');
 | 
			
		||||
 | 
			
		||||
    // Add the private field to the node type.
 | 
			
		||||
    $this->addPrivateField(NodeType::load('moderated_content'));
 | 
			
		||||
 | 
			
		||||
    // Rebuild permissions because hook_node_grants() is implemented by the
 | 
			
		||||
    // node_access_test_empty module.
 | 
			
		||||
    node_access_rebuild();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Verifies that a non-admin user can still access the appropriate pages.
 | 
			
		||||
   */
 | 
			
		||||
  public function testPageAccess(): void {
 | 
			
		||||
    // Initially disable access grant records in
 | 
			
		||||
    // node_access_test_node_access_records().
 | 
			
		||||
    \Drupal::state()->set('node_access_test.private', TRUE);
 | 
			
		||||
 | 
			
		||||
    $this->drupalLogin($this->adminUser);
 | 
			
		||||
 | 
			
		||||
    // Access the node form before moderation is enabled, the publication state
 | 
			
		||||
    // should now be visible.
 | 
			
		||||
    $this->drupalGet('node/add/moderated_content');
 | 
			
		||||
    $this->assertSession()->fieldExists('Published');
 | 
			
		||||
 | 
			
		||||
    // Now enable the workflow.
 | 
			
		||||
    $this->enableModerationThroughUi('moderated_content', 'editorial');
 | 
			
		||||
 | 
			
		||||
    // Access that the status field is no longer visible.
 | 
			
		||||
    $this->drupalGet('node/add/moderated_content');
 | 
			
		||||
    $this->assertSession()->fieldNotExists('Published');
 | 
			
		||||
 | 
			
		||||
    // Create a node to test with.
 | 
			
		||||
    $this->submitForm([
 | 
			
		||||
      'title[0][value]' => 'moderated content',
 | 
			
		||||
      'moderation_state[0][state]' => 'draft',
 | 
			
		||||
    ], 'Save');
 | 
			
		||||
    $node = $this->getNodeByTitle('moderated content');
 | 
			
		||||
    if (!$node) {
 | 
			
		||||
      $this->fail('Test node was not saved correctly.');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    $view_path = 'node/' . $node->id();
 | 
			
		||||
    $edit_path = 'node/' . $node->id() . '/edit';
 | 
			
		||||
    $latest_path = 'node/' . $node->id() . '/latest';
 | 
			
		||||
 | 
			
		||||
    // Now make a new user and verify that the new user's access is correct.
 | 
			
		||||
    $user = $this->createUser([
 | 
			
		||||
      'use editorial transition create_new_draft',
 | 
			
		||||
      'view latest version',
 | 
			
		||||
      'view any unpublished content',
 | 
			
		||||
    ]);
 | 
			
		||||
    $this->drupalLogin($user);
 | 
			
		||||
 | 
			
		||||
    $this->drupalGet($edit_path);
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(403);
 | 
			
		||||
 | 
			
		||||
    $this->drupalGet($latest_path);
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(403);
 | 
			
		||||
    $this->drupalGet($view_path);
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
 | 
			
		||||
    // Publish the node.
 | 
			
		||||
    $this->drupalLogin($this->adminUser);
 | 
			
		||||
    $this->drupalGet($edit_path);
 | 
			
		||||
    $this->submitForm(['moderation_state[0][state]' => 'published'], 'Save');
 | 
			
		||||
 | 
			
		||||
    // Ensure access works correctly for anonymous users.
 | 
			
		||||
    $this->drupalLogout();
 | 
			
		||||
 | 
			
		||||
    $this->drupalGet($edit_path);
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(403);
 | 
			
		||||
 | 
			
		||||
    $this->drupalGet($latest_path);
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(403);
 | 
			
		||||
    $this->drupalGet($view_path);
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
 | 
			
		||||
    // Create a pending revision for the 'Latest revision' tab.
 | 
			
		||||
    $this->drupalLogin($this->adminUser);
 | 
			
		||||
    $this->drupalGet($edit_path);
 | 
			
		||||
    $this->submitForm([
 | 
			
		||||
      'title[0][value]' => 'moderated content revised',
 | 
			
		||||
      'moderation_state[0][state]' => 'draft',
 | 
			
		||||
    ], 'Save');
 | 
			
		||||
 | 
			
		||||
    $this->drupalLogin($user);
 | 
			
		||||
 | 
			
		||||
    $this->drupalGet($edit_path);
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(403);
 | 
			
		||||
 | 
			
		||||
    $this->drupalGet($latest_path);
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
    $this->drupalGet($view_path);
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
 | 
			
		||||
    // Now make another user, who should not be able to see pending revisions.
 | 
			
		||||
    $user = $this->createUser([
 | 
			
		||||
      'use editorial transition create_new_draft',
 | 
			
		||||
    ]);
 | 
			
		||||
    $this->drupalLogin($user);
 | 
			
		||||
 | 
			
		||||
    $this->drupalGet($edit_path);
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(403);
 | 
			
		||||
 | 
			
		||||
    $this->drupalGet($latest_path);
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(403);
 | 
			
		||||
    $this->drupalGet($view_path);
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
 | 
			
		||||
    // Now create a private node that the user is not granted access to by the
 | 
			
		||||
    // node grants, but is granted access via hook_ENTITY_TYPE_access().
 | 
			
		||||
    // @see node_access_test_node_access
 | 
			
		||||
    $node = $this->createNode([
 | 
			
		||||
      'type' => 'moderated_content',
 | 
			
		||||
      'private' => TRUE,
 | 
			
		||||
      'uid' => $this->adminUser->id(),
 | 
			
		||||
    ]);
 | 
			
		||||
    $user = $this->createUser([
 | 
			
		||||
      'use editorial transition publish',
 | 
			
		||||
    ]);
 | 
			
		||||
    $this->drupalLogin($user);
 | 
			
		||||
 | 
			
		||||
    // Grant access to the node via node_access_test_node_access().
 | 
			
		||||
    \Drupal::state()->set('node_access_test.allow_uid', $user->id());
 | 
			
		||||
 | 
			
		||||
    $this->drupalGet($node->toUrl());
 | 
			
		||||
    $this->assertSession()->statusCodeEquals(200);
 | 
			
		||||
 | 
			
		||||
    // Verify the moderation form is in place by publishing the node.
 | 
			
		||||
    $this->submitForm([], 'Apply');
 | 
			
		||||
    $node = \Drupal::entityTypeManager()->getStorage('node')->loadUnchanged($node->id());
 | 
			
		||||
    $this->assertEquals('published', $node->moderation_state->value);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,381 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\content_moderation\Functional;
 | 
			
		||||
 | 
			
		||||
use Drupal\Tests\content_moderation\Traits\ContentModerationTestTrait;
 | 
			
		||||
use Drupal\Tests\views\Functional\ViewTestBase;
 | 
			
		||||
use Drupal\views\Entity\View;
 | 
			
		||||
use Drupal\views\ViewEntityInterface;
 | 
			
		||||
use Drupal\workflows\Entity\Workflow;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests the views 'moderation_state_filter' filter plugin.
 | 
			
		||||
 *
 | 
			
		||||
 * @coversDefaultClass \Drupal\content_moderation\Plugin\views\filter\ModerationStateFilter
 | 
			
		||||
 *
 | 
			
		||||
 * @group content_moderation
 | 
			
		||||
 * @group #slow
 | 
			
		||||
 */
 | 
			
		||||
class ViewsModerationStateFilterTest extends ViewTestBase {
 | 
			
		||||
 | 
			
		||||
  use ContentModerationTestTrait;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = [
 | 
			
		||||
    'node',
 | 
			
		||||
    'content_moderation',
 | 
			
		||||
    'workflows',
 | 
			
		||||
    'workflow_type_test',
 | 
			
		||||
    'entity_test',
 | 
			
		||||
    'language',
 | 
			
		||||
    'content_translation',
 | 
			
		||||
    'views_ui',
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp($import_test_views = TRUE, $modules = []): void {
 | 
			
		||||
    parent::setUp(FALSE, $modules);
 | 
			
		||||
 | 
			
		||||
    $this->drupalCreateContentType([
 | 
			
		||||
      'type' => 'example_a',
 | 
			
		||||
    ]);
 | 
			
		||||
    $this->drupalCreateContentType([
 | 
			
		||||
      'type' => 'example_b',
 | 
			
		||||
    ]);
 | 
			
		||||
    $this->drupalCreateContentType([
 | 
			
		||||
      'type' => 'example_c',
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    $this->createEditorialWorkflow();
 | 
			
		||||
 | 
			
		||||
    $new_workflow = Workflow::create([
 | 
			
		||||
      'type' => 'content_moderation',
 | 
			
		||||
      'id' => 'new_workflow',
 | 
			
		||||
      'label' => 'New workflow',
 | 
			
		||||
    ]);
 | 
			
		||||
    $new_workflow->getTypePlugin()->addState('bar', 'Bar');
 | 
			
		||||
    $new_workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example_c');
 | 
			
		||||
    $new_workflow->save();
 | 
			
		||||
 | 
			
		||||
    $this->drupalLogin($this->drupalCreateUser([
 | 
			
		||||
      'administer workflows',
 | 
			
		||||
      'administer views',
 | 
			
		||||
    ]));
 | 
			
		||||
 | 
			
		||||
    $this->container->get('module_installer')->install(['content_moderation_test_views']);
 | 
			
		||||
 | 
			
		||||
    $new_workflow->getTypePlugin()->removeEntityTypeAndBundle('node', 'example_c');
 | 
			
		||||
    $new_workflow->save();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the dependency handling of the moderation state filter.
 | 
			
		||||
   *
 | 
			
		||||
   * @covers ::calculateDependencies
 | 
			
		||||
   * @covers ::onDependencyRemoval
 | 
			
		||||
   */
 | 
			
		||||
  public function testModerationStateFilterDependencyHandling(): void {
 | 
			
		||||
    // First, check that the view doesn't have any config dependency when there
 | 
			
		||||
    // are no states configured in the filter.
 | 
			
		||||
    $view_id = 'test_content_moderation_state_filter_base_table';
 | 
			
		||||
    $view = View::load($view_id);
 | 
			
		||||
 | 
			
		||||
    $this->assertWorkflowDependencies([], $view);
 | 
			
		||||
    $this->assertTrue($view->status());
 | 
			
		||||
 | 
			
		||||
    // Configure the Editorial workflow for a node bundle, set the filter value
 | 
			
		||||
    // to use one of its states and check that the workflow is now a dependency
 | 
			
		||||
    // of the view.
 | 
			
		||||
    $this->drupalGet('admin/config/workflow/workflows/manage/editorial/type/node');
 | 
			
		||||
    $this->submitForm(['bundles[example_a]' => TRUE], 'Save');
 | 
			
		||||
 | 
			
		||||
    $edit['options[value][]'] = ['editorial-published'];
 | 
			
		||||
    $this->drupalGet("admin/structure/views/nojs/handler/{$view_id}/default/filter/moderation_state");
 | 
			
		||||
    $this->submitForm($edit, 'Apply');
 | 
			
		||||
    $this->drupalGet("admin/structure/views/view/{$view_id}");
 | 
			
		||||
    $this->submitForm([], 'Save');
 | 
			
		||||
 | 
			
		||||
    $view = $this->loadViewUnchanged($view_id);
 | 
			
		||||
    $this->assertWorkflowDependencies(['editorial'], $view);
 | 
			
		||||
    $this->assertTrue($view->status());
 | 
			
		||||
 | 
			
		||||
    // Create another workflow and repeat the checks above.
 | 
			
		||||
    $this->drupalGet('admin/config/workflow/workflows/add');
 | 
			
		||||
    $this->submitForm([
 | 
			
		||||
      'label' => 'Translation',
 | 
			
		||||
      'id' => 'translation',
 | 
			
		||||
      'workflow_type' => 'content_moderation',
 | 
			
		||||
    ], 'Save');
 | 
			
		||||
    $this->drupalGet('admin/config/workflow/workflows/manage/translation/add_state');
 | 
			
		||||
    $this->submitForm([
 | 
			
		||||
      'label' => 'Needs Review',
 | 
			
		||||
      'id' => 'needs_review',
 | 
			
		||||
    ], 'Save');
 | 
			
		||||
    $this->drupalGet('admin/config/workflow/workflows/manage/translation/type/node');
 | 
			
		||||
    $this->submitForm(['bundles[example_b]' => TRUE], 'Save');
 | 
			
		||||
 | 
			
		||||
    $edit['options[value][]'] = ['editorial-published', 'translation-needs_review'];
 | 
			
		||||
    $this->drupalGet("admin/structure/views/nojs/handler/{$view_id}/default/filter/moderation_state");
 | 
			
		||||
    $this->submitForm($edit, 'Apply');
 | 
			
		||||
    $this->drupalGet("admin/structure/views/view/{$view_id}");
 | 
			
		||||
    $this->submitForm([], 'Save');
 | 
			
		||||
 | 
			
		||||
    $view = $this->loadViewUnchanged($view_id);
 | 
			
		||||
    $this->assertWorkflowDependencies(['editorial', 'translation'], $view);
 | 
			
		||||
    $this->assertTrue(isset($view->getDisplay('default')['display_options']['filters']['moderation_state']));
 | 
			
		||||
    $this->assertTrue($view->status());
 | 
			
		||||
 | 
			
		||||
    // Remove the 'Translation' workflow.
 | 
			
		||||
    $this->drupalGet('admin/config/workflow/workflows/manage/translation/delete');
 | 
			
		||||
    $this->submitForm([], 'Delete');
 | 
			
		||||
 | 
			
		||||
    // Check that the view has been disabled, the filter has been deleted, the
 | 
			
		||||
    // view can be saved and there are no more config dependencies.
 | 
			
		||||
    $view = $this->loadViewUnchanged($view_id);
 | 
			
		||||
    $this->assertFalse($view->status());
 | 
			
		||||
    $this->assertFalse(isset($view->getDisplay('default')['display_options']['filters']['moderation_state']));
 | 
			
		||||
    $this->drupalGet("admin/structure/views/view/{$view_id}");
 | 
			
		||||
    $this->submitForm([], 'Save');
 | 
			
		||||
    $this->assertWorkflowDependencies([], $view);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Load a view from the database after it has been modified in a sub-request.
 | 
			
		||||
   *
 | 
			
		||||
   * @param string $view_id
 | 
			
		||||
   *   The view ID.
 | 
			
		||||
   *
 | 
			
		||||
   * @return \Drupal\views\ViewEntityInterface
 | 
			
		||||
   *   A loaded view, bypassing static caches.
 | 
			
		||||
   */
 | 
			
		||||
  public function loadViewUnchanged($view_id) {
 | 
			
		||||
    $this->container->get('cache.config')->deleteAll();
 | 
			
		||||
    $this->container->get('config.factory')->reset();
 | 
			
		||||
    return $this->container->get('entity_type.manager')->getStorage('view')->loadUnchanged($view_id);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the moderation state filter when the configured workflow is changed.
 | 
			
		||||
   *
 | 
			
		||||
   * @dataProvider providerTestWorkflowChanges
 | 
			
		||||
   */
 | 
			
		||||
  public function testWorkflowChanges($view_id): void {
 | 
			
		||||
    // First, apply the Editorial workflow to both of our content types.
 | 
			
		||||
    $this->drupalGet('admin/config/workflow/workflows/manage/editorial/type/node');
 | 
			
		||||
    $this->submitForm([
 | 
			
		||||
      'bundles[example_a]' => TRUE,
 | 
			
		||||
      'bundles[example_b]' => TRUE,
 | 
			
		||||
    ], 'Save');
 | 
			
		||||
    \Drupal::service('entity_type.bundle.info')->clearCachedBundles();
 | 
			
		||||
 | 
			
		||||
    // Update the view and make the default filter not exposed anymore,
 | 
			
		||||
    // otherwise all results will be shown when there are no more moderated
 | 
			
		||||
    // bundles left.
 | 
			
		||||
    $this->drupalGet("admin/structure/views/nojs/handler/{$view_id}/default/filter/moderation_state");
 | 
			
		||||
    $this->submitForm([], 'Hide filter');
 | 
			
		||||
    $this->drupalGet("admin/structure/views/view/{$view_id}");
 | 
			
		||||
    $this->submitForm([], 'Save');
 | 
			
		||||
 | 
			
		||||
    // Add a few nodes in various moderation states.
 | 
			
		||||
    $this->createNode(['type' => 'example_a', 'moderation_state' => 'published']);
 | 
			
		||||
    $this->createNode(['type' => 'example_b', 'moderation_state' => 'published']);
 | 
			
		||||
    $archived_node_a = $this->createNode(['type' => 'example_a', 'moderation_state' => 'archived']);
 | 
			
		||||
    $archived_node_b = $this->createNode(['type' => 'example_b', 'moderation_state' => 'archived']);
 | 
			
		||||
 | 
			
		||||
    // Configure the view to only show nodes in the 'archived' moderation state.
 | 
			
		||||
    $edit['options[value][]'] = ['editorial-archived'];
 | 
			
		||||
    $this->drupalGet("admin/structure/views/nojs/handler/{$view_id}/default/filter/moderation_state");
 | 
			
		||||
    $this->submitForm($edit, 'Apply');
 | 
			
		||||
    $this->drupalGet("admin/structure/views/view/{$view_id}");
 | 
			
		||||
    $this->submitForm([], 'Save');
 | 
			
		||||
 | 
			
		||||
    // Check that only the archived nodes from both bundles are displayed by the
 | 
			
		||||
    // view.
 | 
			
		||||
    $view = $this->loadViewUnchanged($view_id);
 | 
			
		||||
    $this->executeAndAssertIdenticalResultset($view, [['nid' => $archived_node_a->id()], ['nid' => $archived_node_b->id()]], ['nid' => 'nid']);
 | 
			
		||||
 | 
			
		||||
    // Remove the Editorial workflow from one of the bundles.
 | 
			
		||||
    $this->drupalGet('admin/config/workflow/workflows/manage/editorial/type/node');
 | 
			
		||||
    $this->submitForm([
 | 
			
		||||
      'bundles[example_a]' => TRUE,
 | 
			
		||||
      'bundles[example_b]' => FALSE,
 | 
			
		||||
    ], 'Save');
 | 
			
		||||
    \Drupal::service('entity_type.bundle.info')->clearCachedBundles();
 | 
			
		||||
 | 
			
		||||
    $view = $this->loadViewUnchanged($view_id);
 | 
			
		||||
    $this->executeAndAssertIdenticalResultset($view, [['nid' => $archived_node_a->id()]], ['nid' => 'nid']);
 | 
			
		||||
 | 
			
		||||
    // Check that the view can still be edited and saved without any
 | 
			
		||||
    // intervention.
 | 
			
		||||
    $this->drupalGet("admin/structure/views/view/{$view_id}");
 | 
			
		||||
    $this->submitForm([], 'Save');
 | 
			
		||||
 | 
			
		||||
    // Remove the Editorial workflow from both bundles.
 | 
			
		||||
    $this->drupalGet('admin/config/workflow/workflows/manage/editorial/type/node');
 | 
			
		||||
    $this->submitForm([
 | 
			
		||||
      'bundles[example_a]' => FALSE,
 | 
			
		||||
      'bundles[example_b]' => FALSE,
 | 
			
		||||
    ], 'Save');
 | 
			
		||||
    \Drupal::service('entity_type.bundle.info')->clearCachedBundles();
 | 
			
		||||
 | 
			
		||||
    // Check that the view doesn't return any result.
 | 
			
		||||
    $view = $this->loadViewUnchanged($view_id);
 | 
			
		||||
    $this->executeAndAssertIdenticalResultset($view, [], []);
 | 
			
		||||
 | 
			
		||||
    // Check that the view contains a broken filter, since the moderation_state
 | 
			
		||||
    // field was removed from the entity type.
 | 
			
		||||
    $this->drupalGet("admin/structure/views/view/{$view_id}");
 | 
			
		||||
    $this->submitForm([], 'Save');
 | 
			
		||||
    $this->assertSession()->pageTextContains("Broken/missing handler");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Execute a view and assert the expected results.
 | 
			
		||||
   *
 | 
			
		||||
   * @param \Drupal\views\ViewEntityInterface $view_entity
 | 
			
		||||
   *   A view configuration entity.
 | 
			
		||||
   * @param array $expected
 | 
			
		||||
   *   An expected result set.
 | 
			
		||||
   * @param array $column_map
 | 
			
		||||
   *   An associative array mapping the columns of the result set from the view
 | 
			
		||||
   *   (as keys) and the expected result set (as values).
 | 
			
		||||
   */
 | 
			
		||||
  protected function executeAndAssertIdenticalResultset(ViewEntityInterface $view_entity, $expected, $column_map): void {
 | 
			
		||||
    $executable = $this->container->get('views.executable')->get($view_entity);
 | 
			
		||||
    $this->executeView($executable);
 | 
			
		||||
    $this->assertIdenticalResultset($executable, $expected, $column_map);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Data provider for testWorkflowChanges.
 | 
			
		||||
   *
 | 
			
		||||
   * @return string[]
 | 
			
		||||
   *   An array of view IDs.
 | 
			
		||||
   */
 | 
			
		||||
  public static function providerTestWorkflowChanges() {
 | 
			
		||||
    return [
 | 
			
		||||
      'view on base table, filter on base table' => [
 | 
			
		||||
        'test_content_moderation_state_filter_base_table',
 | 
			
		||||
      ],
 | 
			
		||||
      'view on base table, filter on revision table' => [
 | 
			
		||||
        'test_content_moderation_state_filter_base_table_filter_on_revision',
 | 
			
		||||
      ],
 | 
			
		||||
    ];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the content moderation state filter caching is correct.
 | 
			
		||||
   */
 | 
			
		||||
  public function testFilterRenderCache(): void {
 | 
			
		||||
    // Initially all states of the workflow are displayed.
 | 
			
		||||
    $this->drupalGet('admin/config/workflow/workflows/manage/editorial/type/node');
 | 
			
		||||
    $this->submitForm(['bundles[example_a]' => TRUE], 'Save');
 | 
			
		||||
    $this->assertFilterStates(['All', 'editorial-draft', 'editorial-published', 'editorial-archived']);
 | 
			
		||||
 | 
			
		||||
    // Adding a new state to the editorial workflow will display that state in
 | 
			
		||||
    // the list of filters.
 | 
			
		||||
    $this->drupalGet('admin/config/workflow/workflows/manage/editorial/add_state');
 | 
			
		||||
    $this->submitForm([
 | 
			
		||||
      'label' => 'Foo',
 | 
			
		||||
      'id' => 'foo',
 | 
			
		||||
    ], 'Save');
 | 
			
		||||
    $this->assertFilterStates(['All', 'editorial-draft', 'editorial-published', 'editorial-archived', 'editorial-foo']);
 | 
			
		||||
 | 
			
		||||
    // Adding a second workflow to nodes will also show new states.
 | 
			
		||||
    $this->drupalGet('admin/config/workflow/workflows/manage/new_workflow/type/node');
 | 
			
		||||
    $this->submitForm(['bundles[example_b]' => TRUE], 'Save');
 | 
			
		||||
    $this->assertFilterStates(['All', 'editorial-draft', 'editorial-published', 'editorial-archived', 'editorial-foo', 'new_workflow-draft', 'new_workflow-published', 'new_workflow-bar']);
 | 
			
		||||
 | 
			
		||||
    // Add a few more states and change the exposed filter to allow multiple
 | 
			
		||||
    // selections so we can check that the size of the select element does not
 | 
			
		||||
    // exceed 8 options.
 | 
			
		||||
    $this->drupalGet('admin/config/workflow/workflows/manage/editorial/add_state');
 | 
			
		||||
    $this->submitForm([
 | 
			
		||||
      'label' => 'Foo 2',
 | 
			
		||||
      'id' => 'foo2',
 | 
			
		||||
    ], 'Save');
 | 
			
		||||
    $this->drupalGet('admin/config/workflow/workflows/manage/editorial/add_state');
 | 
			
		||||
    $this->submitForm([
 | 
			
		||||
      'label' => 'Foo 3',
 | 
			
		||||
      'id' => 'foo3',
 | 
			
		||||
    ], 'Save');
 | 
			
		||||
 | 
			
		||||
    $view_id = 'test_content_moderation_state_filter_base_table';
 | 
			
		||||
    $edit['options[expose][multiple]'] = TRUE;
 | 
			
		||||
    $this->drupalGet("admin/structure/views/nojs/handler/{$view_id}/default/filter/moderation_state");
 | 
			
		||||
    $this->submitForm($edit, 'Apply');
 | 
			
		||||
    $this->drupalGet("admin/structure/views/view/{$view_id}");
 | 
			
		||||
    $this->submitForm([], 'Save');
 | 
			
		||||
 | 
			
		||||
    $this->assertFilterStates(['editorial-draft', 'editorial-published', 'editorial-archived', 'editorial-foo', 'editorial-foo2', 'editorial-foo3', 'new_workflow-draft', 'new_workflow-published', 'new_workflow-bar'], TRUE);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Assert the states which appear in the filter.
 | 
			
		||||
   *
 | 
			
		||||
   * @param array $states
 | 
			
		||||
   *   The states which should appear in the filter.
 | 
			
		||||
   * @param bool $check_size
 | 
			
		||||
   *   (optional) Whether to check that size of the select element is not
 | 
			
		||||
   *   greater than 8. Defaults to FALSE.
 | 
			
		||||
   *
 | 
			
		||||
   * @internal
 | 
			
		||||
   */
 | 
			
		||||
  protected function assertFilterStates(array $states, bool $check_size = FALSE): void {
 | 
			
		||||
    $this->drupalGet('/filter-test-path');
 | 
			
		||||
 | 
			
		||||
    $assert_session = $this->assertSession();
 | 
			
		||||
 | 
			
		||||
    // Check that the select contains the correct number of options.
 | 
			
		||||
    $assert_session->elementsCount('css', '#edit-default-revision-state option', count($states));
 | 
			
		||||
 | 
			
		||||
    // Check that the size of the select element does not exceed 8 options.
 | 
			
		||||
    if ($check_size) {
 | 
			
		||||
      $this->assertGreaterThan(8, count($states));
 | 
			
		||||
      $assert_session->elementAttributeContains('css', '#edit-default-revision-state', 'size', '8');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Check that an option exists for each of the expected states.
 | 
			
		||||
    foreach ($states as $state) {
 | 
			
		||||
      $assert_session->optionExists('Default Revision State', $state);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Asserts the views dependencies on workflow config entities.
 | 
			
		||||
   *
 | 
			
		||||
   * @param string[] $workflow_ids
 | 
			
		||||
   *   An array of workflow IDs to check.
 | 
			
		||||
   * @param \Drupal\views\ViewEntityInterface $view
 | 
			
		||||
   *   A view configuration object.
 | 
			
		||||
   *
 | 
			
		||||
   * @internal
 | 
			
		||||
   */
 | 
			
		||||
  protected function assertWorkflowDependencies(array $workflow_ids, ViewEntityInterface $view): void {
 | 
			
		||||
    $dependencies = $view->getDependencies();
 | 
			
		||||
 | 
			
		||||
    $expected = [];
 | 
			
		||||
    foreach (Workflow::loadMultiple($workflow_ids) as $workflow) {
 | 
			
		||||
      $expected[] = $workflow->getConfigDependencyName();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if ($expected) {
 | 
			
		||||
      $this->assertSame($expected, $dependencies['config']);
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
      $this->assertTrue(!isset($dependencies['config']));
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,141 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\content_moderation\Functional;
 | 
			
		||||
 | 
			
		||||
use Drupal\Tests\workspaces\Functional\WorkspaceTestUtilities;
 | 
			
		||||
use Drupal\workspaces\Entity\Workspace;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests Workspaces together with Content Moderation.
 | 
			
		||||
 *
 | 
			
		||||
 * @group content_moderation
 | 
			
		||||
 * @group workspaces
 | 
			
		||||
 */
 | 
			
		||||
class WorkspaceContentModerationIntegrationTest extends ModerationStateTestBase {
 | 
			
		||||
 | 
			
		||||
  use WorkspaceTestUtilities;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = ['node', 'workspaces'];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function getAdministratorPermissions(): array {
 | 
			
		||||
    return array_merge($this->permissions, [
 | 
			
		||||
      'bypass node access',
 | 
			
		||||
      'administer workspaces',
 | 
			
		||||
    ]);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
 | 
			
		||||
    $this->adminUser = $this->drupalCreateUser($this->getAdministratorPermissions());
 | 
			
		||||
    $this->drupalLogin($this->adminUser);
 | 
			
		||||
 | 
			
		||||
    // Enable moderation on Article node type.
 | 
			
		||||
    $this->createContentTypeFromUi('Article', 'article', TRUE);
 | 
			
		||||
 | 
			
		||||
    $this->setupWorkspaceSwitcherBlock();
 | 
			
		||||
    $this->createWorkspaceThroughUi('Stage', 'stage');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests moderating nodes in a workspace.
 | 
			
		||||
   */
 | 
			
		||||
  public function testModerationInWorkspace(): void {
 | 
			
		||||
    $stage = Workspace::load('stage');
 | 
			
		||||
    $this->switchToWorkspace($stage);
 | 
			
		||||
 | 
			
		||||
    // Create two nodes, a published and a draft one.
 | 
			
		||||
    $this->drupalGet('node/add/article');
 | 
			
		||||
    $this->submitForm([
 | 
			
		||||
      'title[0][value]' => 'First article - published',
 | 
			
		||||
      'moderation_state[0][state]' => 'published',
 | 
			
		||||
    ], 'Save');
 | 
			
		||||
    $this->drupalGet('node/add/article');
 | 
			
		||||
    $this->submitForm([
 | 
			
		||||
      'title[0][value]' => 'Second article - draft',
 | 
			
		||||
      'moderation_state[0][state]' => 'draft',
 | 
			
		||||
    ], 'Save');
 | 
			
		||||
 | 
			
		||||
    $first_article = $this->drupalGetNodeByTitle('First article - published', TRUE);
 | 
			
		||||
    $this->assertEquals('published', $first_article->moderation_state->value);
 | 
			
		||||
    $this->assertTrue($first_article->isPublished());
 | 
			
		||||
 | 
			
		||||
    $second_article = $this->drupalGetNodeByTitle('Second article - draft', TRUE);
 | 
			
		||||
    $this->assertEquals('draft', $second_article->moderation_state->value);
 | 
			
		||||
    $this->assertFalse($second_article->isPublished());
 | 
			
		||||
 | 
			
		||||
    // Check that neither of them are published in Live.
 | 
			
		||||
    $this->switchToLive();
 | 
			
		||||
    $first_article = $this->drupalGetNodeByTitle('First article - published', TRUE);
 | 
			
		||||
    $this->assertFalse($first_article->isPublished());
 | 
			
		||||
 | 
			
		||||
    $second_article = $this->drupalGetNodeByTitle('Second article - draft', TRUE);
 | 
			
		||||
    $this->assertFalse($second_article->isPublished());
 | 
			
		||||
 | 
			
		||||
    // Switch back to Stage.
 | 
			
		||||
    $this->switchToWorkspace($stage);
 | 
			
		||||
 | 
			
		||||
    // Take the first node through various moderation states.
 | 
			
		||||
    $this->drupalGet('/node/1/edit');
 | 
			
		||||
    $this->assertEquals('Current state Published', $this->cssSelect('#edit-moderation-state-0-current')[0]->getText());
 | 
			
		||||
 | 
			
		||||
    $this->submitForm([
 | 
			
		||||
      'title[0][value]' => 'First article - draft',
 | 
			
		||||
      'moderation_state[0][state]' => 'draft',
 | 
			
		||||
    ], 'Save');
 | 
			
		||||
 | 
			
		||||
    $this->drupalGet('/node/1');
 | 
			
		||||
    $this->assertSession()->pageTextContains('First article - draft');
 | 
			
		||||
 | 
			
		||||
    $this->drupalGet('/node/1/edit');
 | 
			
		||||
    $this->assertEquals('Current state Draft', $this->cssSelect('#edit-moderation-state-0-current')[0]->getText());
 | 
			
		||||
 | 
			
		||||
    $this->submitForm([
 | 
			
		||||
      'title[0][value]' => 'First article - published',
 | 
			
		||||
      'moderation_state[0][state]' => 'published',
 | 
			
		||||
    ], 'Save');
 | 
			
		||||
 | 
			
		||||
    $this->drupalGet('/node/1/edit');
 | 
			
		||||
    $this->submitForm([
 | 
			
		||||
      'title[0][value]' => 'First article - archived',
 | 
			
		||||
      'moderation_state[0][state]' => 'archived',
 | 
			
		||||
    ], 'Save');
 | 
			
		||||
 | 
			
		||||
    $this->drupalGet('/node/1');
 | 
			
		||||
    $this->assertSession()->pageTextContains('First article - archived');
 | 
			
		||||
 | 
			
		||||
    // Get the second node to a default revision state and publish the
 | 
			
		||||
    // workspace.
 | 
			
		||||
    $this->drupalGet('/node/2/edit');
 | 
			
		||||
    $this->submitForm([
 | 
			
		||||
      'title[0][value]' => 'Second article - published',
 | 
			
		||||
      'moderation_state[0][state]' => 'published',
 | 
			
		||||
    ], 'Save');
 | 
			
		||||
 | 
			
		||||
    $stage->publish();
 | 
			
		||||
 | 
			
		||||
    // The admin user can see unpublished nodes.
 | 
			
		||||
    $this->drupalGet('/node/1');
 | 
			
		||||
    $this->assertSession()->pageTextContains('First article - archived');
 | 
			
		||||
 | 
			
		||||
    $this->drupalGet('/node/2');
 | 
			
		||||
    $this->assertSession()->pageTextContains('Second article - published');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,126 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\content_moderation\Kernel\ConfigAction;
 | 
			
		||||
 | 
			
		||||
use Drupal\Component\Plugin\Exception\PluginNotFoundException;
 | 
			
		||||
use Drupal\Core\Config\Action\ConfigActionException;
 | 
			
		||||
use Drupal\Core\Recipe\Recipe;
 | 
			
		||||
use Drupal\Core\Recipe\RecipeRunner;
 | 
			
		||||
use Drupal\FunctionalTests\Core\Recipe\RecipeTestTrait;
 | 
			
		||||
use Drupal\KernelTests\KernelTestBase;
 | 
			
		||||
use Drupal\Tests\node\Traits\ContentTypeCreationTrait;
 | 
			
		||||
use Drupal\Tests\taxonomy\Traits\TaxonomyTestTrait;
 | 
			
		||||
use Drupal\workflows\Entity\Workflow;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @covers \Drupal\content_moderation\Plugin\ConfigAction\AddModeration
 | 
			
		||||
 * @covers \Drupal\content_moderation\Plugin\ConfigAction\AddModerationDeriver
 | 
			
		||||
 * @group content_moderation
 | 
			
		||||
 * @group Recipe
 | 
			
		||||
 */
 | 
			
		||||
class AddModerationConfigActionTest extends KernelTestBase {
 | 
			
		||||
 | 
			
		||||
  use ContentTypeCreationTrait;
 | 
			
		||||
  use RecipeTestTrait {
 | 
			
		||||
    createRecipe as traitCreateRecipe;
 | 
			
		||||
  }
 | 
			
		||||
  use TaxonomyTestTrait;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = [
 | 
			
		||||
    'field',
 | 
			
		||||
    'node',
 | 
			
		||||
    'system',
 | 
			
		||||
    'taxonomy',
 | 
			
		||||
    'text',
 | 
			
		||||
    'user',
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests adding entity types and bundles to a workflow.
 | 
			
		||||
   */
 | 
			
		||||
  public function testAddEntityTypeAndBundle(): void {
 | 
			
		||||
    $this->installEntitySchema('node');
 | 
			
		||||
    $this->installConfig('node');
 | 
			
		||||
 | 
			
		||||
    $this->createContentType(['type' => 'a']);
 | 
			
		||||
    $this->createContentType(['type' => 'b']);
 | 
			
		||||
    $this->createContentType(['type' => 'c']);
 | 
			
		||||
    $this->createVocabulary(['vid' => 'tags']);
 | 
			
		||||
 | 
			
		||||
    $recipe = $this->createRecipe('workflows.workflow.editorial');
 | 
			
		||||
    RecipeRunner::processRecipe($recipe);
 | 
			
		||||
 | 
			
		||||
    /** @var \Drupal\content_moderation\Plugin\WorkflowType\ContentModerationInterface $plugin */
 | 
			
		||||
    $plugin = Workflow::load('editorial')?->getTypePlugin();
 | 
			
		||||
    $this->assertSame(['a', 'b'], $plugin->getBundlesForEntityType('node'));
 | 
			
		||||
    $this->assertSame(['tags'], $plugin->getBundlesForEntityType('taxonomy_term'));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests that the workflow must be of type Content Moderation.
 | 
			
		||||
   */
 | 
			
		||||
  public function testWorkflowMustBeContentModeration(): void {
 | 
			
		||||
    $this->enableModules(['workflows', 'workflow_type_test']);
 | 
			
		||||
 | 
			
		||||
    $workflow = Workflow::create([
 | 
			
		||||
      'id' => 'test',
 | 
			
		||||
      'label' => 'Test workflow',
 | 
			
		||||
      'type' => 'workflow_type_test',
 | 
			
		||||
    ]);
 | 
			
		||||
    $workflow->save();
 | 
			
		||||
 | 
			
		||||
    $recipe = $this->createRecipe($workflow->getConfigDependencyName());
 | 
			
		||||
    $this->expectException(ConfigActionException::class);
 | 
			
		||||
    $this->expectExceptionMessage("The add_moderation:addNodeTypes config action only works with Content Moderation workflows.");
 | 
			
		||||
    RecipeRunner::processRecipe($recipe);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests that the action only targets workflows.
 | 
			
		||||
   */
 | 
			
		||||
  public function testActionOnlyTargetsWorkflows(): void {
 | 
			
		||||
    $recipe = $this->createRecipe('user.role.anonymous');
 | 
			
		||||
    $this->expectException(PluginNotFoundException::class);
 | 
			
		||||
    $this->expectExceptionMessage('The "user_role" entity does not support the "addNodeTypes" config action.');
 | 
			
		||||
    RecipeRunner::processRecipe($recipe);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests that the derived config action definitions have correct admin labels.
 | 
			
		||||
   */
 | 
			
		||||
  public function testDeriverAdminLabel(): void {
 | 
			
		||||
    $this->enableModules(['workflows', 'content_moderation']);
 | 
			
		||||
 | 
			
		||||
    /** @var array<string, array{admin_label: \Stringable}> $definitions */
 | 
			
		||||
    $definitions = $this->container->get('plugin.manager.config_action')
 | 
			
		||||
      ->getDefinitions();
 | 
			
		||||
 | 
			
		||||
    $this->assertSame('Add moderation to all content types', (string) $definitions['add_moderation:addNodeTypes']['admin_label']);
 | 
			
		||||
    $this->assertSame('Add moderation to all vocabularies', (string) $definitions['add_moderation:addTaxonomyVocabularies']['admin_label']);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Creates a recipe configuration for adding entity types and bundles to a workflow.
 | 
			
		||||
   */
 | 
			
		||||
  private function createRecipe(string $config_name): Recipe {
 | 
			
		||||
    $recipe = <<<YAML
 | 
			
		||||
name: 'Add entity types and bundles to workflow'
 | 
			
		||||
recipes:
 | 
			
		||||
  - core/recipes/editorial_workflow
 | 
			
		||||
config:
 | 
			
		||||
  actions:
 | 
			
		||||
    $config_name:
 | 
			
		||||
      addNodeTypes:
 | 
			
		||||
        - a
 | 
			
		||||
        - b
 | 
			
		||||
      addTaxonomyVocabularies: '*'
 | 
			
		||||
YAML;
 | 
			
		||||
    return $this->traitCreateRecipe($recipe);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,99 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\content_moderation\Kernel;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Cache\CacheBackendInterface;
 | 
			
		||||
use Drupal\Core\Session\UserSession;
 | 
			
		||||
use Drupal\KernelTests\KernelTestBase;
 | 
			
		||||
use Drupal\node\Entity\NodeType;
 | 
			
		||||
use Drupal\Tests\content_moderation\Traits\ContentModerationTestTrait;
 | 
			
		||||
use Drupal\Tests\node\Traits\NodeCreationTrait;
 | 
			
		||||
use Drupal\Tests\user\Traits\UserCreationTrait;
 | 
			
		||||
use Drupal\user\Entity\Role;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests content moderation access.
 | 
			
		||||
 *
 | 
			
		||||
 * @group content_moderation
 | 
			
		||||
 */
 | 
			
		||||
class ContentModerationAccessTest extends KernelTestBase {
 | 
			
		||||
 | 
			
		||||
  use NodeCreationTrait;
 | 
			
		||||
  use UserCreationTrait;
 | 
			
		||||
  use ContentModerationTestTrait;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = [
 | 
			
		||||
    'content_moderation',
 | 
			
		||||
    'filter',
 | 
			
		||||
    'node',
 | 
			
		||||
    'system',
 | 
			
		||||
    'user',
 | 
			
		||||
    'workflows',
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
 | 
			
		||||
    $this->installEntitySchema('content_moderation_state');
 | 
			
		||||
    $this->installEntitySchema('node');
 | 
			
		||||
    $this->installEntitySchema('user');
 | 
			
		||||
    $this->installConfig(['content_moderation', 'filter']);
 | 
			
		||||
    $this->installSchema('node', ['node_access']);
 | 
			
		||||
 | 
			
		||||
    // Add a moderated node type.
 | 
			
		||||
    $node_type = NodeType::create([
 | 
			
		||||
      'type' => 'page',
 | 
			
		||||
      'name' => 'Page',
 | 
			
		||||
    ]);
 | 
			
		||||
    $node_type->save();
 | 
			
		||||
    $workflow = $this->createEditorialWorkflow();
 | 
			
		||||
    $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'page');
 | 
			
		||||
    $workflow->save();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests access cacheability.
 | 
			
		||||
   */
 | 
			
		||||
  public function testAccessCacheability(): void {
 | 
			
		||||
    $node = $this->createNode(['type' => 'page']);
 | 
			
		||||
 | 
			
		||||
    /** @var \Drupal\user\RoleInterface $authenticated */
 | 
			
		||||
    $authenticated = Role::create([
 | 
			
		||||
      'id' => 'authenticated',
 | 
			
		||||
      'label' => 'Authenticated',
 | 
			
		||||
    ]);
 | 
			
		||||
    $authenticated->grantPermission('access content');
 | 
			
		||||
    $authenticated->grantPermission('edit any page content');
 | 
			
		||||
    $authenticated->save();
 | 
			
		||||
 | 
			
		||||
    $account = new UserSession([
 | 
			
		||||
      'uid' => 2,
 | 
			
		||||
      'roles' => ['authenticated'],
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    $result = $node->access('update', $account, TRUE);
 | 
			
		||||
    $this->assertFalse($result->isAllowed());
 | 
			
		||||
    $this->assertEqualsCanonicalizing(['user.permissions'], $result->getCacheContexts());
 | 
			
		||||
    $this->assertEqualsCanonicalizing(['config:workflows.workflow.editorial', 'node:' . $node->id()], $result->getCacheTags());
 | 
			
		||||
    $this->assertEquals(CacheBackendInterface::CACHE_PERMANENT, $result->getCacheMaxAge());
 | 
			
		||||
 | 
			
		||||
    $authenticated->grantPermission('use editorial transition create_new_draft');
 | 
			
		||||
    $authenticated->save();
 | 
			
		||||
 | 
			
		||||
    \Drupal::entityTypeManager()->getAccessControlHandler('node')->resetCache();
 | 
			
		||||
    $result = $node->access('update', $account, TRUE);
 | 
			
		||||
    $this->assertTrue($result->isAllowed());
 | 
			
		||||
    $this->assertEqualsCanonicalizing(['user.permissions'], $result->getCacheContexts());
 | 
			
		||||
    $this->assertEqualsCanonicalizing(['config:workflows.workflow.editorial', 'node:' . $node->id()], $result->getCacheTags());
 | 
			
		||||
    $this->assertEquals(CacheBackendInterface::CACHE_PERMANENT, $result->getCacheMaxAge());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,168 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\content_moderation\Kernel;
 | 
			
		||||
 | 
			
		||||
use Drupal\content_moderation\Permissions;
 | 
			
		||||
use Drupal\KernelTests\KernelTestBase;
 | 
			
		||||
use Drupal\workflows\Entity\Workflow;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Test to ensure content moderation permissions are generated correctly.
 | 
			
		||||
 *
 | 
			
		||||
 * @group content_moderation
 | 
			
		||||
 */
 | 
			
		||||
class ContentModerationPermissionsTest extends KernelTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = [
 | 
			
		||||
    'workflows',
 | 
			
		||||
    'content_moderation',
 | 
			
		||||
    'workflow_type_test',
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
    $this->installEntitySchema('workflow');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests permissions generated by content moderation.
 | 
			
		||||
   *
 | 
			
		||||
   * @dataProvider permissionsTestCases
 | 
			
		||||
   */
 | 
			
		||||
  public function testPermissions($workflow, $permissions): void {
 | 
			
		||||
    Workflow::create($workflow)->save();
 | 
			
		||||
    $this->assertEquals($permissions, (new Permissions())->transitionPermissions());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Test cases for ::testPermissions.
 | 
			
		||||
   *
 | 
			
		||||
   * @return array
 | 
			
		||||
   *   Content moderation permissions based test cases.
 | 
			
		||||
   */
 | 
			
		||||
  public static function permissionsTestCases() {
 | 
			
		||||
    return [
 | 
			
		||||
      'Simple Content Moderation Workflow' => [
 | 
			
		||||
        [
 | 
			
		||||
          'id' => 'simple_workflow',
 | 
			
		||||
          'label' => 'Simple Workflow',
 | 
			
		||||
          'type' => 'content_moderation',
 | 
			
		||||
          'type_settings' => [
 | 
			
		||||
            'states' => [
 | 
			
		||||
              'draft' => [
 | 
			
		||||
                'label' => 'Draft',
 | 
			
		||||
                'published' => FALSE,
 | 
			
		||||
                'default_revision' => FALSE,
 | 
			
		||||
                'weight' => 0,
 | 
			
		||||
              ],
 | 
			
		||||
              'published' => [
 | 
			
		||||
                'label' => 'Published',
 | 
			
		||||
                'published' => TRUE,
 | 
			
		||||
                'default_revision' => TRUE,
 | 
			
		||||
                'weight' => 1,
 | 
			
		||||
              ],
 | 
			
		||||
              'archived' => [
 | 
			
		||||
                'label' => 'Archived',
 | 
			
		||||
                'published' => FALSE,
 | 
			
		||||
                'default_revision' => TRUE,
 | 
			
		||||
                'weight' => 2,
 | 
			
		||||
              ],
 | 
			
		||||
            ],
 | 
			
		||||
            'transitions' => [
 | 
			
		||||
              'create_new_draft' => [
 | 
			
		||||
                'label' => 'Create New Draft',
 | 
			
		||||
                'to' => 'draft',
 | 
			
		||||
                'weight' => 0,
 | 
			
		||||
                'from' => [
 | 
			
		||||
                  'draft',
 | 
			
		||||
                  'published',
 | 
			
		||||
                ],
 | 
			
		||||
              ],
 | 
			
		||||
              'publish' => [
 | 
			
		||||
                'label' => 'Publish',
 | 
			
		||||
                'to' => 'published',
 | 
			
		||||
                'weight' => 1,
 | 
			
		||||
                'from' => [
 | 
			
		||||
                  'draft',
 | 
			
		||||
                  'published',
 | 
			
		||||
                ],
 | 
			
		||||
              ],
 | 
			
		||||
              'archive' => [
 | 
			
		||||
                'label' => 'Archive',
 | 
			
		||||
                'to' => 'archived',
 | 
			
		||||
                'weight' => 2,
 | 
			
		||||
                'from' => [
 | 
			
		||||
                  'published',
 | 
			
		||||
                ],
 | 
			
		||||
              ],
 | 
			
		||||
            ],
 | 
			
		||||
          ],
 | 
			
		||||
        ],
 | 
			
		||||
        [
 | 
			
		||||
          'use simple_workflow transition publish' => [
 | 
			
		||||
            'title' => 'Simple Workflow workflow: Use Publish transition.',
 | 
			
		||||
            'description' => 'Move content from Draft, Published states to Published state.',
 | 
			
		||||
            'dependencies' => [
 | 
			
		||||
              'config' => [
 | 
			
		||||
                'workflows.workflow.simple_workflow',
 | 
			
		||||
              ],
 | 
			
		||||
            ],
 | 
			
		||||
          ],
 | 
			
		||||
          'use simple_workflow transition create_new_draft' => [
 | 
			
		||||
            'title' => 'Simple Workflow workflow: Use Create New Draft transition.',
 | 
			
		||||
            'description' => 'Move content from Draft, Published states to Draft state.',
 | 
			
		||||
            'dependencies' => [
 | 
			
		||||
              'config' => [
 | 
			
		||||
                'workflows.workflow.simple_workflow',
 | 
			
		||||
              ],
 | 
			
		||||
            ],
 | 
			
		||||
          ],
 | 
			
		||||
          'use simple_workflow transition archive' => [
 | 
			
		||||
            'title' => 'Simple Workflow workflow: Use Archive transition.',
 | 
			
		||||
            'description' => 'Move content from Published state to Archived state.',
 | 
			
		||||
            'dependencies' => [
 | 
			
		||||
              'config' => [
 | 
			
		||||
                'workflows.workflow.simple_workflow',
 | 
			
		||||
              ],
 | 
			
		||||
            ],
 | 
			
		||||
          ],
 | 
			
		||||
        ],
 | 
			
		||||
      ],
 | 
			
		||||
      'Non Content Moderation Workflow' => [
 | 
			
		||||
        [
 | 
			
		||||
          'id' => 'morning',
 | 
			
		||||
          'label' => 'Morning',
 | 
			
		||||
          'type' => 'workflow_type_test',
 | 
			
		||||
          'transitions' => [
 | 
			
		||||
            'drink_coffee' => [
 | 
			
		||||
              'label' => 'Drink Coffee',
 | 
			
		||||
              'from' => ['tired'],
 | 
			
		||||
              'to' => 'awake',
 | 
			
		||||
              'weight' => 0,
 | 
			
		||||
            ],
 | 
			
		||||
          ],
 | 
			
		||||
          'states' => [
 | 
			
		||||
            'awake' => [
 | 
			
		||||
              'label' => 'Awake',
 | 
			
		||||
              'weight' => -5,
 | 
			
		||||
            ],
 | 
			
		||||
            'tired' => [
 | 
			
		||||
              'label' => 'Tired',
 | 
			
		||||
              'weight' => -0,
 | 
			
		||||
            ],
 | 
			
		||||
          ],
 | 
			
		||||
        ],
 | 
			
		||||
        [],
 | 
			
		||||
      ],
 | 
			
		||||
    ];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,109 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\content_moderation\Kernel;
 | 
			
		||||
 | 
			
		||||
use Drupal\content_moderation\Entity\ContentModerationState;
 | 
			
		||||
use Drupal\KernelTests\KernelTestBase;
 | 
			
		||||
use Drupal\Tests\content_moderation\Traits\ContentModerationTestTrait;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests Content Moderation with entities that get re-saved automatically.
 | 
			
		||||
 *
 | 
			
		||||
 * @group content_moderation
 | 
			
		||||
 */
 | 
			
		||||
class ContentModerationResaveTest extends KernelTestBase {
 | 
			
		||||
 | 
			
		||||
  use ContentModerationTestTrait;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = [
 | 
			
		||||
    // Make sure the test module is listed first as module weights do not apply
 | 
			
		||||
    // for kernel tests.
 | 
			
		||||
    /* @see \content_moderation_test_resave_install() */
 | 
			
		||||
    'content_moderation_test_resave',
 | 
			
		||||
    'content_moderation',
 | 
			
		||||
    'entity_test',
 | 
			
		||||
    'user',
 | 
			
		||||
    'workflows',
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The content moderation state entity storage.
 | 
			
		||||
   *
 | 
			
		||||
   * @var \Drupal\Core\Entity\EntityStorageInterface
 | 
			
		||||
   */
 | 
			
		||||
  protected $contentModerationStateStorage;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The entity storage for the entity type used in the test.
 | 
			
		||||
   *
 | 
			
		||||
   * @var \Drupal\Core\Entity\EntityStorageInterface
 | 
			
		||||
   */
 | 
			
		||||
  protected $entityStorage;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The state service.
 | 
			
		||||
   *
 | 
			
		||||
   * @var \Drupal\Core\State\StateInterface
 | 
			
		||||
   */
 | 
			
		||||
  protected $state;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
 | 
			
		||||
    $entity_type_id = 'entity_test_rev';
 | 
			
		||||
 | 
			
		||||
    $this->installEntitySchema('content_moderation_state');
 | 
			
		||||
    $this->installEntitySchema($entity_type_id);
 | 
			
		||||
 | 
			
		||||
    $workflow = $this->createEditorialWorkflow();
 | 
			
		||||
    $this->addEntityTypeAndBundleToWorkflow($workflow, $entity_type_id, $entity_type_id);
 | 
			
		||||
 | 
			
		||||
    /** @var \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager */
 | 
			
		||||
    $entity_type_manager = $this->container->get('entity_type.manager');
 | 
			
		||||
    $this->contentModerationStateStorage = $entity_type_manager->getStorage('content_moderation_state');
 | 
			
		||||
    $this->entityStorage = $entity_type_manager->getStorage($entity_type_id);
 | 
			
		||||
    $this->state = $this->container->get('state');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests that Content Moderation works with entities being resaved.
 | 
			
		||||
   */
 | 
			
		||||
  public function testContentModerationResave(): void {
 | 
			
		||||
    $entity = $this->entityStorage->create();
 | 
			
		||||
    $this->assertSame('draft', $entity->get('moderation_state')->value);
 | 
			
		||||
    $this->assertNull(\Drupal::state()->get('content_moderation_test_resave'));
 | 
			
		||||
    $this->assertNull(ContentModerationState::loadFromModeratedEntity($entity));
 | 
			
		||||
    $content_moderation_state_query = $this->contentModerationStateStorage
 | 
			
		||||
      ->getQuery()
 | 
			
		||||
      ->accessCheck(FALSE)
 | 
			
		||||
      ->count();
 | 
			
		||||
    $this->assertSame(0, (int) $content_moderation_state_query->execute());
 | 
			
		||||
    $content_moderation_state_revision_query = $this->contentModerationStateStorage
 | 
			
		||||
      ->getQuery()
 | 
			
		||||
      ->accessCheck(FALSE)
 | 
			
		||||
      ->allRevisions()
 | 
			
		||||
      ->count();
 | 
			
		||||
    $this->assertSame(0, (int) $content_moderation_state_revision_query->execute());
 | 
			
		||||
 | 
			
		||||
    // The test module will re-save the entity in its hook_insert()
 | 
			
		||||
    // implementation creating the content moderation state entity before
 | 
			
		||||
    // Content Moderation's hook_insert() has run for the initial save
 | 
			
		||||
    // operation.
 | 
			
		||||
    $entity->save();
 | 
			
		||||
    $this->assertSame('draft', $entity->get('moderation_state')->value);
 | 
			
		||||
    $this->assertTrue(\Drupal::state()->get('content_moderation_test_resave'));
 | 
			
		||||
    $content_moderation_state = ContentModerationState::loadFromModeratedEntity($entity);
 | 
			
		||||
    $this->assertInstanceOf(ContentModerationState::class, $content_moderation_state);
 | 
			
		||||
    $this->assertSame(1, (int) $content_moderation_state_query->execute());
 | 
			
		||||
    $this->assertSame(1, (int) $content_moderation_state_revision_query->execute());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,54 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\content_moderation\Kernel;
 | 
			
		||||
 | 
			
		||||
use Drupal\content_moderation\Entity\ContentModerationState;
 | 
			
		||||
use Drupal\KernelTests\KernelTestBase;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @coversDefaultClass \Drupal\content_moderation\ContentModerationStateAccessControlHandler
 | 
			
		||||
 * @group content_moderation
 | 
			
		||||
 */
 | 
			
		||||
class ContentModerationStateAccessControlHandlerTest extends KernelTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = [
 | 
			
		||||
    'content_moderation',
 | 
			
		||||
    'workflows',
 | 
			
		||||
    'user',
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The content_moderation_state access control handler.
 | 
			
		||||
   *
 | 
			
		||||
   * @var \Drupal\Core\Entity\EntityAccessControlHandlerInterface
 | 
			
		||||
   */
 | 
			
		||||
  protected $accessControlHandler;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
    $this->installEntitySchema('content_moderation_state');
 | 
			
		||||
    $this->installEntitySchema('user');
 | 
			
		||||
    $this->accessControlHandler = $this->container->get('entity_type.manager')->getAccessControlHandler('content_moderation_state');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @covers ::checkAccess
 | 
			
		||||
   * @covers ::checkCreateAccess
 | 
			
		||||
   */
 | 
			
		||||
  public function testHandler(): void {
 | 
			
		||||
    $entity = ContentModerationState::create([]);
 | 
			
		||||
    $this->assertFalse($this->accessControlHandler->access($entity, 'view'));
 | 
			
		||||
    $this->assertFalse($this->accessControlHandler->access($entity, 'update'));
 | 
			
		||||
    $this->assertFalse($this->accessControlHandler->access($entity, 'delete'));
 | 
			
		||||
    $this->assertFalse($this->accessControlHandler->createAccess());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,41 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\content_moderation\Kernel;
 | 
			
		||||
 | 
			
		||||
use Drupal\Component\Plugin\Exception\PluginNotFoundException;
 | 
			
		||||
use Drupal\KernelTests\KernelTestBase;
 | 
			
		||||
use Drupal\rest\Entity\RestResourceConfig;
 | 
			
		||||
use Drupal\rest\RestResourceConfigInterface;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @group content_moderation
 | 
			
		||||
 */
 | 
			
		||||
class ContentModerationStateResourceTest extends KernelTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = ['serialization', 'rest', 'content_moderation'];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @see \Drupal\content_moderation\Entity\ContentModerationState
 | 
			
		||||
   */
 | 
			
		||||
  public function testCreateContentModerationStateResource(): void {
 | 
			
		||||
    $this->expectException(PluginNotFoundException::class);
 | 
			
		||||
    $this->expectExceptionMessage('The "entity:content_moderation_state" plugin does not exist.');
 | 
			
		||||
    RestResourceConfig::create([
 | 
			
		||||
      'id' => 'entity.content_moderation_state',
 | 
			
		||||
      'granularity' => RestResourceConfigInterface::RESOURCE_GRANULARITY,
 | 
			
		||||
      'configuration' => [
 | 
			
		||||
        'methods' => ['GET'],
 | 
			
		||||
        'formats' => ['json'],
 | 
			
		||||
        'authentication' => ['cookie'],
 | 
			
		||||
      ],
 | 
			
		||||
    ])
 | 
			
		||||
      ->enable()
 | 
			
		||||
      ->save();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,149 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\content_moderation\Kernel;
 | 
			
		||||
 | 
			
		||||
use Drupal\content_moderation\Entity\ContentModerationState;
 | 
			
		||||
use Drupal\KernelTests\KernelTestBase;
 | 
			
		||||
use Drupal\node\Entity\Node;
 | 
			
		||||
use Drupal\node\Entity\NodeType;
 | 
			
		||||
use Drupal\Tests\content_moderation\Traits\ContentModerationTestTrait;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Test the ContentModerationState storage schema.
 | 
			
		||||
 *
 | 
			
		||||
 * @coversDefaultClass \Drupal\content_moderation\ContentModerationStateStorageSchema
 | 
			
		||||
 * @group content_moderation
 | 
			
		||||
 */
 | 
			
		||||
class ContentModerationStateStorageSchemaTest extends KernelTestBase {
 | 
			
		||||
 | 
			
		||||
  use ContentModerationTestTrait;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = [
 | 
			
		||||
    'node',
 | 
			
		||||
    'content_moderation',
 | 
			
		||||
    'user',
 | 
			
		||||
    'system',
 | 
			
		||||
    'text',
 | 
			
		||||
    'workflows',
 | 
			
		||||
    'entity_test',
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
 | 
			
		||||
    $this->installSchema('node', 'node_access');
 | 
			
		||||
    $this->installEntitySchema('node');
 | 
			
		||||
    $this->installEntitySchema('entity_test');
 | 
			
		||||
    $this->installEntitySchema('user');
 | 
			
		||||
    $this->installEntitySchema('content_moderation_state');
 | 
			
		||||
    $this->installConfig('content_moderation');
 | 
			
		||||
 | 
			
		||||
    NodeType::create([
 | 
			
		||||
      'type' => 'example',
 | 
			
		||||
      'name' => 'Example',
 | 
			
		||||
    ])->save();
 | 
			
		||||
    $workflow = $this->createEditorialWorkflow();
 | 
			
		||||
    $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example');
 | 
			
		||||
    $workflow->save();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the ContentModerationState unique keys.
 | 
			
		||||
   *
 | 
			
		||||
   * @covers ::getEntitySchema
 | 
			
		||||
   */
 | 
			
		||||
  public function testUniqueKeys(): void {
 | 
			
		||||
    // Create a node which will create a new ContentModerationState entity.
 | 
			
		||||
    $node = Node::create([
 | 
			
		||||
      'title' => 'Test title',
 | 
			
		||||
      'type' => 'example',
 | 
			
		||||
      'moderation_state' => 'draft',
 | 
			
		||||
    ]);
 | 
			
		||||
    $node->save();
 | 
			
		||||
 | 
			
		||||
    // Ensure an exception when all values match.
 | 
			
		||||
    $this->assertStorageException([
 | 
			
		||||
      'content_entity_type_id' => $node->getEntityTypeId(),
 | 
			
		||||
      'content_entity_id' => $node->id(),
 | 
			
		||||
      'content_entity_revision_id' => $node->getRevisionId(),
 | 
			
		||||
    ], TRUE);
 | 
			
		||||
 | 
			
		||||
    // No exception for the same values, with a different langcode.
 | 
			
		||||
    $this->assertStorageException([
 | 
			
		||||
      'content_entity_type_id' => $node->getEntityTypeId(),
 | 
			
		||||
      'content_entity_id' => $node->id(),
 | 
			
		||||
      'content_entity_revision_id' => $node->getRevisionId(),
 | 
			
		||||
      'langcode' => 'de',
 | 
			
		||||
    ], FALSE);
 | 
			
		||||
 | 
			
		||||
    // A different workflow should not trigger an exception.
 | 
			
		||||
    $this->assertStorageException([
 | 
			
		||||
      'content_entity_type_id' => $node->getEntityTypeId(),
 | 
			
		||||
      'content_entity_id' => $node->id(),
 | 
			
		||||
      'content_entity_revision_id' => $node->getRevisionId(),
 | 
			
		||||
      'workflow' => 'foo',
 | 
			
		||||
    ], FALSE);
 | 
			
		||||
 | 
			
		||||
    // Different entity types should not trigger an exception.
 | 
			
		||||
    $this->assertStorageException([
 | 
			
		||||
      'content_entity_type_id' => 'entity_test',
 | 
			
		||||
      'content_entity_id' => $node->id(),
 | 
			
		||||
      'content_entity_revision_id' => $node->getRevisionId(),
 | 
			
		||||
    ], FALSE);
 | 
			
		||||
 | 
			
		||||
    // Different entity and revision IDs should not trigger an exception.
 | 
			
		||||
    $this->assertStorageException([
 | 
			
		||||
      'content_entity_type_id' => $node->getEntityTypeId(),
 | 
			
		||||
      'content_entity_id' => 9999,
 | 
			
		||||
      'content_entity_revision_id' => 9999,
 | 
			
		||||
    ], FALSE);
 | 
			
		||||
 | 
			
		||||
    // Creating a version of the entity with a previously used, but not current
 | 
			
		||||
    // revision ID should trigger an exception.
 | 
			
		||||
    $old_revision_id = $node->getRevisionId();
 | 
			
		||||
    $node->setNewRevision(TRUE);
 | 
			
		||||
    $node->title = 'Updated title';
 | 
			
		||||
    $node->moderation_state = 'published';
 | 
			
		||||
    $node->save();
 | 
			
		||||
    $this->assertStorageException([
 | 
			
		||||
      'content_entity_type_id' => $node->getEntityTypeId(),
 | 
			
		||||
      'content_entity_id' => $node->id(),
 | 
			
		||||
      'content_entity_revision_id' => $old_revision_id,
 | 
			
		||||
    ], TRUE);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Assert if a storage exception is triggered when saving a given entity.
 | 
			
		||||
   *
 | 
			
		||||
   * @param array $values
 | 
			
		||||
   *   An array of entity values.
 | 
			
		||||
   * @param bool $has_exception
 | 
			
		||||
   *   If an exception should be triggered when saving the entity.
 | 
			
		||||
   *
 | 
			
		||||
   * @internal
 | 
			
		||||
   */
 | 
			
		||||
  protected function assertStorageException(array $values, bool $has_exception): void {
 | 
			
		||||
    $defaults = [
 | 
			
		||||
      'moderation_state' => 'draft',
 | 
			
		||||
      'workflow' => 'editorial',
 | 
			
		||||
    ];
 | 
			
		||||
    $entity = ContentModerationState::create($values + $defaults);
 | 
			
		||||
    $exception_triggered = FALSE;
 | 
			
		||||
    try {
 | 
			
		||||
      ContentModerationState::updateOrCreateFromEntity($entity);
 | 
			
		||||
    }
 | 
			
		||||
    catch (\Exception) {
 | 
			
		||||
      $exception_triggered = TRUE;
 | 
			
		||||
    }
 | 
			
		||||
    $this->assertEquals($has_exception, $exception_triggered);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,871 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\content_moderation\Kernel;
 | 
			
		||||
 | 
			
		||||
use Drupal\content_moderation\Entity\ContentModerationState;
 | 
			
		||||
use Drupal\Core\Entity\EntityDefinitionUpdateManagerInterface;
 | 
			
		||||
use Drupal\Core\Entity\EntityInterface;
 | 
			
		||||
use Drupal\Core\Entity\EntityPublishedInterface;
 | 
			
		||||
use Drupal\Core\Entity\EntityStorageException;
 | 
			
		||||
use Drupal\Core\Language\LanguageInterface;
 | 
			
		||||
use Drupal\Core\State\StateInterface;
 | 
			
		||||
use Drupal\entity_test\EntityTestHelper;
 | 
			
		||||
use Drupal\KernelTests\KernelTestBase;
 | 
			
		||||
use Drupal\language\Entity\ConfigurableLanguage;
 | 
			
		||||
use Drupal\node\Entity\Node;
 | 
			
		||||
use Drupal\Tests\content_moderation\Traits\ContentModerationTestTrait;
 | 
			
		||||
use Drupal\Tests\node\Traits\ContentTypeCreationTrait;
 | 
			
		||||
use Drupal\Tests\system\Functional\Entity\Traits\EntityDefinitionTestTrait;
 | 
			
		||||
use Drupal\workflows\Entity\Workflow;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests links between a content entity and a content_moderation_state entity.
 | 
			
		||||
 *
 | 
			
		||||
 * @group content_moderation
 | 
			
		||||
 */
 | 
			
		||||
class ContentModerationStateTest extends KernelTestBase {
 | 
			
		||||
 | 
			
		||||
  use ContentModerationTestTrait;
 | 
			
		||||
  use EntityDefinitionTestTrait;
 | 
			
		||||
  use ContentTypeCreationTrait;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = [
 | 
			
		||||
    'entity_test',
 | 
			
		||||
    'node',
 | 
			
		||||
    'block',
 | 
			
		||||
    'block_content',
 | 
			
		||||
    'media',
 | 
			
		||||
    'media_test_source',
 | 
			
		||||
    'image',
 | 
			
		||||
    'file',
 | 
			
		||||
    'field',
 | 
			
		||||
    'filter',
 | 
			
		||||
    'content_moderation',
 | 
			
		||||
    'user',
 | 
			
		||||
    'system',
 | 
			
		||||
    'language',
 | 
			
		||||
    'content_translation',
 | 
			
		||||
    'text',
 | 
			
		||||
    'workflows',
 | 
			
		||||
    'path_alias',
 | 
			
		||||
    'taxonomy',
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @var \Drupal\Core\Entity\EntityTypeManager
 | 
			
		||||
   */
 | 
			
		||||
  protected $entityTypeManager;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The state object.
 | 
			
		||||
   *
 | 
			
		||||
   * @var \Drupal\Core\State\StateInterface
 | 
			
		||||
   */
 | 
			
		||||
  protected StateInterface $state;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The entity definition update manager.
 | 
			
		||||
   *
 | 
			
		||||
   * @var \Drupal\Core\Entity\EntityDefinitionUpdateManagerInterface
 | 
			
		||||
   */
 | 
			
		||||
  protected EntityDefinitionUpdateManagerInterface $entityDefinitionUpdateManager;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The ID of the revisionable entity type used in the tests.
 | 
			
		||||
   *
 | 
			
		||||
   * @var string
 | 
			
		||||
   */
 | 
			
		||||
  protected $revEntityTypeId = 'entity_test_rev';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
 | 
			
		||||
    $this->installSchema('node', 'node_access');
 | 
			
		||||
    $this->installEntitySchema('node');
 | 
			
		||||
    $this->installEntitySchema('user');
 | 
			
		||||
    $this->installEntitySchema($this->revEntityTypeId);
 | 
			
		||||
    $this->installEntitySchema('entity_test_no_bundle');
 | 
			
		||||
    $this->installEntitySchema('entity_test_mulrevpub');
 | 
			
		||||
    $this->installEntitySchema('block_content');
 | 
			
		||||
    $this->installEntitySchema('media');
 | 
			
		||||
    $this->installEntitySchema('file');
 | 
			
		||||
    $this->installEntitySchema('taxonomy_term');
 | 
			
		||||
    $this->installEntitySchema('content_moderation_state');
 | 
			
		||||
    $this->installConfig('content_moderation');
 | 
			
		||||
    $this->installSchema('file', 'file_usage');
 | 
			
		||||
    $this->installConfig(['field', 'file', 'filter', 'image', 'media', 'node', 'system']);
 | 
			
		||||
 | 
			
		||||
    // Add the French language.
 | 
			
		||||
    ConfigurableLanguage::createFromLangcode('fr')->save();
 | 
			
		||||
 | 
			
		||||
    $this->entityTypeManager = $this->container->get('entity_type.manager');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests basic monolingual content moderation through the API.
 | 
			
		||||
   */
 | 
			
		||||
  public function testBasicModeration(): void {
 | 
			
		||||
    foreach (static::basicModerationTestCases() as $case) {
 | 
			
		||||
      [$entity_type_id] = $case;
 | 
			
		||||
      $this->doTestBasicModeration($entity_type_id);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests basic monolingual content moderation through the API.
 | 
			
		||||
   */
 | 
			
		||||
  protected function doTestBasicModeration($entity_type_id): void {
 | 
			
		||||
    $entity = $this->createEntity($entity_type_id, 'draft');
 | 
			
		||||
    $entity = $this->reloadEntity($entity);
 | 
			
		||||
    $this->assertEquals('draft', $entity->moderation_state->value);
 | 
			
		||||
 | 
			
		||||
    $entity->moderation_state->value = 'published';
 | 
			
		||||
    $entity->save();
 | 
			
		||||
 | 
			
		||||
    $entity = $this->reloadEntity($entity);
 | 
			
		||||
    $this->assertEquals('published', $entity->moderation_state->value);
 | 
			
		||||
 | 
			
		||||
    // Change the state without saving the node.
 | 
			
		||||
    $content_moderation_state = ContentModerationState::load(1);
 | 
			
		||||
    $content_moderation_state->set('moderation_state', 'draft');
 | 
			
		||||
    $content_moderation_state->setNewRevision(TRUE);
 | 
			
		||||
    $content_moderation_state->save();
 | 
			
		||||
 | 
			
		||||
    $entity = $this->reloadEntity($entity, 3);
 | 
			
		||||
    $this->assertEquals('draft', $entity->moderation_state->value);
 | 
			
		||||
    if ($entity instanceof EntityPublishedInterface) {
 | 
			
		||||
      $this->assertFalse($entity->isPublished());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Check the default revision.
 | 
			
		||||
    $this->assertDefaultRevision($entity, 2);
 | 
			
		||||
 | 
			
		||||
    $entity->moderation_state->value = 'published';
 | 
			
		||||
    $entity->save();
 | 
			
		||||
 | 
			
		||||
    $entity = $this->reloadEntity($entity, 4);
 | 
			
		||||
    $this->assertEquals('published', $entity->moderation_state->value);
 | 
			
		||||
 | 
			
		||||
    // Check the default revision.
 | 
			
		||||
    $this->assertDefaultRevision($entity, 4);
 | 
			
		||||
 | 
			
		||||
    // Update the node to archived which will then be the default revision.
 | 
			
		||||
    $entity->moderation_state->value = 'archived';
 | 
			
		||||
    $entity->save();
 | 
			
		||||
 | 
			
		||||
    // Revert to the previous (published) revision.
 | 
			
		||||
    /** @var \Drupal\Core\Entity\RevisionableStorageInterface $entity_storage */
 | 
			
		||||
    $entity_storage = $this->entityTypeManager->getStorage($entity_type_id);
 | 
			
		||||
    $previous_revision = $entity_storage->loadRevision(4);
 | 
			
		||||
    $previous_revision->isDefaultRevision(TRUE);
 | 
			
		||||
    $previous_revision->setNewRevision(TRUE);
 | 
			
		||||
    $previous_revision->save();
 | 
			
		||||
 | 
			
		||||
    // Check the default revision.
 | 
			
		||||
    $this->assertDefaultRevision($entity, 6);
 | 
			
		||||
 | 
			
		||||
    // Set an invalid moderation state.
 | 
			
		||||
    $this->expectException(EntityStorageException::class);
 | 
			
		||||
    $entity->moderation_state->value = 'foobar';
 | 
			
		||||
    $entity->save();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Test cases for basic moderation test.
 | 
			
		||||
   */
 | 
			
		||||
  public static function basicModerationTestCases() {
 | 
			
		||||
    return [
 | 
			
		||||
      'Nodes' => [
 | 
			
		||||
        'node',
 | 
			
		||||
      ],
 | 
			
		||||
      'Taxonomy term' => [
 | 
			
		||||
        'taxonomy_term',
 | 
			
		||||
      ],
 | 
			
		||||
      'Block content' => [
 | 
			
		||||
        'block_content',
 | 
			
		||||
      ],
 | 
			
		||||
      'Media' => [
 | 
			
		||||
        'media',
 | 
			
		||||
      ],
 | 
			
		||||
      'Test entity - revisions, data table, and published interface' => [
 | 
			
		||||
        'entity_test_mulrevpub',
 | 
			
		||||
      ],
 | 
			
		||||
      'Entity Test with revisions' => [
 | 
			
		||||
        'entity_test_rev',
 | 
			
		||||
      ],
 | 
			
		||||
      'Entity without bundle' => [
 | 
			
		||||
        'entity_test_no_bundle',
 | 
			
		||||
      ],
 | 
			
		||||
    ];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests removal of content moderation state entity.
 | 
			
		||||
   */
 | 
			
		||||
  public function testContentModerationStateDataRemoval(): void {
 | 
			
		||||
    foreach (static::basicModerationTestCases() as $case) {
 | 
			
		||||
      [$entity_type_id] = $case;
 | 
			
		||||
      $this->doTestContentModerationStateDataRemoval($entity_type_id);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests removal of content moderation state entity.
 | 
			
		||||
   */
 | 
			
		||||
  public function doTestContentModerationStateDataRemoval($entity_type_id): void {
 | 
			
		||||
    /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
 | 
			
		||||
    $entity = $this->createEntity($entity_type_id);
 | 
			
		||||
    $entity = $this->reloadEntity($entity);
 | 
			
		||||
    $entity->delete();
 | 
			
		||||
    $content_moderation_state = ContentModerationState::loadFromModeratedEntity($entity);
 | 
			
		||||
    $this->assertNull($content_moderation_state);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests removal of content moderation state entity revisions.
 | 
			
		||||
   */
 | 
			
		||||
  public function testContentModerationStateRevisionDataRemoval(): void {
 | 
			
		||||
    foreach (static::basicModerationTestCases() as $case) {
 | 
			
		||||
      [$entity_type_id] = $case;
 | 
			
		||||
      $this->doTestContentModerationStateRevisionDataRemoval($entity_type_id);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests removal of content moderation state entity revisions.
 | 
			
		||||
   */
 | 
			
		||||
  public function doTestContentModerationStateRevisionDataRemoval($entity_type_id): void {
 | 
			
		||||
    /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
 | 
			
		||||
    $entity = $this->createEntity($entity_type_id);
 | 
			
		||||
    $revision_1 = clone $entity;
 | 
			
		||||
    $this->assertNotNull(ContentModerationState::loadFromModeratedEntity($revision_1));
 | 
			
		||||
 | 
			
		||||
    // Create a second revision.
 | 
			
		||||
    $entity = $this->reloadEntity($entity);
 | 
			
		||||
    $entity->setNewRevision(TRUE);
 | 
			
		||||
    $entity->save();
 | 
			
		||||
    $revision_2 = clone $entity;
 | 
			
		||||
 | 
			
		||||
    // Create a third revision.
 | 
			
		||||
    $entity = $this->reloadEntity($entity);
 | 
			
		||||
    $entity->setNewRevision(TRUE);
 | 
			
		||||
    $entity->save();
 | 
			
		||||
    $revision_3 = clone $entity;
 | 
			
		||||
 | 
			
		||||
    // Delete the second revision and check that its content moderation state is
 | 
			
		||||
    // removed as well, while the content moderation states for revisions 1 and
 | 
			
		||||
    // 3 are kept in place.
 | 
			
		||||
    /** @var \Drupal\Core\Entity\RevisionableStorageInterface $entity_storage */
 | 
			
		||||
    $entity_storage = $this->entityTypeManager->getStorage($entity_type_id);
 | 
			
		||||
    $entity_storage->deleteRevision($revision_2->getRevisionId());
 | 
			
		||||
 | 
			
		||||
    $this->assertNotNull(ContentModerationState::loadFromModeratedEntity($revision_1));
 | 
			
		||||
    $this->assertNull(ContentModerationState::loadFromModeratedEntity($revision_2));
 | 
			
		||||
    $this->assertNotNull(ContentModerationState::loadFromModeratedEntity($revision_3));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests removal of content moderation state pending entity revisions.
 | 
			
		||||
   */
 | 
			
		||||
  public function testContentModerationStatePendingRevisionDataRemoval(): void {
 | 
			
		||||
    foreach (static::basicModerationTestCases() as $case) {
 | 
			
		||||
      [$entity_type_id] = $case;
 | 
			
		||||
      $this->doTestContentModerationStatePendingRevisionDataRemoval($entity_type_id);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests removal of content moderation state pending entity revisions.
 | 
			
		||||
   */
 | 
			
		||||
  public function doTestContentModerationStatePendingRevisionDataRemoval($entity_type_id): void {
 | 
			
		||||
    $entity = $this->createEntity($entity_type_id, 'published');
 | 
			
		||||
    $entity->setNewRevision(TRUE);
 | 
			
		||||
    $entity->moderation_state = 'draft';
 | 
			
		||||
    $entity->save();
 | 
			
		||||
 | 
			
		||||
    $content_moderation_state = ContentModerationState::loadFromModeratedEntity($entity);
 | 
			
		||||
    $this->assertNotEmpty($content_moderation_state);
 | 
			
		||||
 | 
			
		||||
    /** @var \Drupal\Core\Entity\RevisionableStorageInterface $entity_storage */
 | 
			
		||||
    $entity_storage = $this->entityTypeManager->getStorage($entity_type_id);
 | 
			
		||||
    $entity_storage->deleteRevision($entity->getRevisionId());
 | 
			
		||||
 | 
			
		||||
    $content_moderation_state = ContentModerationState::loadFromModeratedEntity($entity);
 | 
			
		||||
    $this->assertNull($content_moderation_state);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests removal of content moderation state entities for preexisting content.
 | 
			
		||||
   */
 | 
			
		||||
  public function testExistingContentModerationStateDataRemoval(): void {
 | 
			
		||||
    /** @var \Drupal\Core\Entity\RevisionableStorageInterface $storage */
 | 
			
		||||
    $storage = $this->entityTypeManager->getStorage('entity_test_mulrevpub');
 | 
			
		||||
 | 
			
		||||
    $entity = $this->createEntity('entity_test_mulrevpub', 'published', FALSE);
 | 
			
		||||
    $original_revision_id = $entity->getRevisionId();
 | 
			
		||||
 | 
			
		||||
    $workflow = $this->createEditorialWorkflow();
 | 
			
		||||
    $this->addEntityTypeAndBundleToWorkflow($workflow, $entity->getEntityTypeId(), $entity->bundle());
 | 
			
		||||
 | 
			
		||||
    $entity = $this->reloadEntity($entity);
 | 
			
		||||
    $entity->moderation_state = 'draft';
 | 
			
		||||
    $entity->save();
 | 
			
		||||
 | 
			
		||||
    $storage->deleteRevision($entity->getRevisionId());
 | 
			
		||||
 | 
			
		||||
    $entity = $this->reloadEntity($entity);
 | 
			
		||||
    $this->assertEquals('published', $entity->moderation_state->value);
 | 
			
		||||
    $this->assertEquals($original_revision_id, $storage->getLatestRevisionId($entity->id()));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests removal of content moderation state translations.
 | 
			
		||||
   */
 | 
			
		||||
  public function testContentModerationStateTranslationDataRemoval(): void {
 | 
			
		||||
    foreach (static::basicModerationTestCases() as $case) {
 | 
			
		||||
      [$entity_type_id] = $case;
 | 
			
		||||
      $this->doTestContentModerationStateTranslationDataRemoval($entity_type_id);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests removal of content moderation state translations.
 | 
			
		||||
   */
 | 
			
		||||
  public function doTestContentModerationStateTranslationDataRemoval($entity_type_id): void {
 | 
			
		||||
    // Test content moderation state translation deletion.
 | 
			
		||||
    if ($this->entityTypeManager->getDefinition($entity_type_id)->isTranslatable()) {
 | 
			
		||||
      /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
 | 
			
		||||
      $entity = $this->createEntity($entity_type_id, 'published');
 | 
			
		||||
      $langcode = 'fr';
 | 
			
		||||
      $translation = $entity->addTranslation($langcode, [$entity->getEntityType()->getKey('label') => 'French title test']);
 | 
			
		||||
      // Make sure we add values for all of the required fields.
 | 
			
		||||
      if ($entity_type_id == 'block_content') {
 | 
			
		||||
        $translation->info = $this->randomString();
 | 
			
		||||
      }
 | 
			
		||||
      $translation->save();
 | 
			
		||||
      $content_moderation_state = ContentModerationState::loadFromModeratedEntity($entity);
 | 
			
		||||
      $this->assertTrue($content_moderation_state->hasTranslation($langcode));
 | 
			
		||||
      $entity->removeTranslation($langcode);
 | 
			
		||||
      $entity->save();
 | 
			
		||||
      $content_moderation_state = ContentModerationState::loadFromModeratedEntity($entity);
 | 
			
		||||
      $this->assertFalse($content_moderation_state->hasTranslation($langcode));
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests basic multilingual content moderation through the API.
 | 
			
		||||
   */
 | 
			
		||||
  public function testMultilingualModeration(): void {
 | 
			
		||||
    $this->createContentType([
 | 
			
		||||
      'type' => 'example',
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    $workflow = $this->createEditorialWorkflow();
 | 
			
		||||
    $this->addEntityTypeAndBundleToWorkflow($workflow, 'node', 'example');
 | 
			
		||||
 | 
			
		||||
    $english_node = Node::create([
 | 
			
		||||
      'type' => 'example',
 | 
			
		||||
      'title' => 'Test title',
 | 
			
		||||
    ]);
 | 
			
		||||
    // Revision 1 (en).
 | 
			
		||||
    $english_node
 | 
			
		||||
      ->setUnpublished()
 | 
			
		||||
      ->save();
 | 
			
		||||
    $this->assertEquals('draft', $english_node->moderation_state->value);
 | 
			
		||||
    $this->assertFalse($english_node->isPublished());
 | 
			
		||||
 | 
			
		||||
    // Create a French translation.
 | 
			
		||||
    $french_node = $english_node->addTranslation('fr', ['title' => 'French title']);
 | 
			
		||||
    $french_node->setUnpublished();
 | 
			
		||||
    // Revision 2 (fr).
 | 
			
		||||
    $french_node->save();
 | 
			
		||||
    $french_node = $this->reloadEntity($english_node)->getTranslation('fr');
 | 
			
		||||
    $this->assertEquals('draft', $french_node->moderation_state->value);
 | 
			
		||||
    $this->assertFalse($french_node->isPublished());
 | 
			
		||||
 | 
			
		||||
    // Move English node to create another draft.
 | 
			
		||||
    $english_node = $this->reloadEntity($english_node);
 | 
			
		||||
    $english_node->moderation_state->value = 'draft';
 | 
			
		||||
    // Revision 3 (en, fr).
 | 
			
		||||
    $english_node->save();
 | 
			
		||||
    $english_node = $this->reloadEntity($english_node);
 | 
			
		||||
    $this->assertEquals('draft', $english_node->moderation_state->value);
 | 
			
		||||
 | 
			
		||||
    // French node should still be in draft.
 | 
			
		||||
    $french_node = $this->reloadEntity($english_node)->getTranslation('fr');
 | 
			
		||||
    $this->assertEquals('draft', $french_node->moderation_state->value);
 | 
			
		||||
 | 
			
		||||
    // Publish the French node.
 | 
			
		||||
    $french_node->moderation_state->value = 'published';
 | 
			
		||||
    // Revision 4 (en, fr).
 | 
			
		||||
    $french_node->save();
 | 
			
		||||
    $french_node = $this->reloadEntity($french_node)->getTranslation('fr');
 | 
			
		||||
    $this->assertTrue($french_node->isPublished());
 | 
			
		||||
    $this->assertEquals('published', $french_node->moderation_state->value);
 | 
			
		||||
    $this->assertTrue($french_node->isPublished());
 | 
			
		||||
    $english_node = $french_node->getTranslation('en');
 | 
			
		||||
    $this->assertEquals('draft', $english_node->moderation_state->value);
 | 
			
		||||
 | 
			
		||||
    // Publish the English node.
 | 
			
		||||
    $english_node->moderation_state->value = 'published';
 | 
			
		||||
    // Revision 5 (en, fr).
 | 
			
		||||
    $english_node->save();
 | 
			
		||||
    $english_node = $this->reloadEntity($english_node);
 | 
			
		||||
    $this->assertTrue($english_node->isPublished());
 | 
			
		||||
 | 
			
		||||
    // Move the French node back to draft.
 | 
			
		||||
    $french_node = $this->reloadEntity($english_node)->getTranslation('fr');
 | 
			
		||||
    $this->assertTrue($french_node->isPublished());
 | 
			
		||||
    $french_node->moderation_state->value = 'draft';
 | 
			
		||||
    // Revision 6 (en, fr).
 | 
			
		||||
    $french_node->save();
 | 
			
		||||
    $french_node = $this->reloadEntity($english_node, 6)->getTranslation('fr');
 | 
			
		||||
    $this->assertFalse($french_node->isPublished());
 | 
			
		||||
    $this->assertTrue($french_node->getTranslation('en')->isPublished());
 | 
			
		||||
 | 
			
		||||
    // Republish the French node.
 | 
			
		||||
    $french_node->moderation_state->value = 'published';
 | 
			
		||||
    // Revision 7 (en, fr).
 | 
			
		||||
    $french_node->save();
 | 
			
		||||
    $french_node = $this->reloadEntity($english_node)->getTranslation('fr');
 | 
			
		||||
    $this->assertTrue($french_node->isPublished());
 | 
			
		||||
 | 
			
		||||
    // Change the EN state without saving the node.
 | 
			
		||||
    $content_moderation_state = ContentModerationState::loadFromModeratedEntity($english_node);
 | 
			
		||||
    $content_moderation_state->set('moderation_state', 'draft');
 | 
			
		||||
    $content_moderation_state->setNewRevision(TRUE);
 | 
			
		||||
    // Revision 8 (en, fr).
 | 
			
		||||
    $content_moderation_state->save();
 | 
			
		||||
    $english_node = $this->reloadEntity($french_node, $french_node->getRevisionId() + 1);
 | 
			
		||||
 | 
			
		||||
    $this->assertEquals('draft', $english_node->moderation_state->value);
 | 
			
		||||
    $french_node = $this->reloadEntity($english_node)->getTranslation('fr');
 | 
			
		||||
    $this->assertEquals('published', $french_node->moderation_state->value);
 | 
			
		||||
 | 
			
		||||
    // This should unpublish the French node.
 | 
			
		||||
    $content_moderation_state = ContentModerationState::loadFromModeratedEntity($english_node);
 | 
			
		||||
    $content_moderation_state = $content_moderation_state->getTranslation('fr');
 | 
			
		||||
    $content_moderation_state->set('moderation_state', 'draft');
 | 
			
		||||
    $content_moderation_state->setNewRevision(TRUE);
 | 
			
		||||
    // Revision 9 (en, fr).
 | 
			
		||||
    $content_moderation_state->save();
 | 
			
		||||
 | 
			
		||||
    $english_node = $this->reloadEntity($english_node, $english_node->getRevisionId());
 | 
			
		||||
    $this->assertEquals('draft', $english_node->moderation_state->value);
 | 
			
		||||
    $french_node = $this->reloadEntity($english_node, '9')->getTranslation('fr');
 | 
			
		||||
    $this->assertEquals('draft', $french_node->moderation_state->value);
 | 
			
		||||
    // Switching the moderation state to an unpublished state should update the
 | 
			
		||||
    // entity.
 | 
			
		||||
    $this->assertFalse($french_node->isPublished());
 | 
			
		||||
 | 
			
		||||
    // Check that revision 7 is still the default one for the node.
 | 
			
		||||
    $this->assertDefaultRevision($english_node, 7);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests moderation when the moderation_state field has a config override.
 | 
			
		||||
   */
 | 
			
		||||
  public function testModerationWithFieldConfigOverride(): void {
 | 
			
		||||
    $this->createContentType([
 | 
			
		||||
      'type' => 'test_type',
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    $workflow = $this->createEditorialWorkflow();
 | 
			
		||||
    $this->addEntityTypeAndBundleToWorkflow($workflow, 'node', 'test_type');
 | 
			
		||||
 | 
			
		||||
    $fields = $this->container->get('entity_field.manager')->getFieldDefinitions('node', 'test_type');
 | 
			
		||||
    $field_config = $fields['moderation_state']->getConfig('test_type');
 | 
			
		||||
    $field_config->setLabel('Field Override!');
 | 
			
		||||
    $field_config->save();
 | 
			
		||||
 | 
			
		||||
    $node = Node::create([
 | 
			
		||||
      'title' => 'Test node',
 | 
			
		||||
      'type' => 'test_type',
 | 
			
		||||
    ]);
 | 
			
		||||
    $node->save();
 | 
			
		||||
    $this->assertFalse($node->isPublished());
 | 
			
		||||
    $this->assertEquals('draft', $node->moderation_state->value);
 | 
			
		||||
 | 
			
		||||
    $node->moderation_state = 'published';
 | 
			
		||||
    $node->save();
 | 
			
		||||
    $this->assertTrue($node->isPublished());
 | 
			
		||||
    $this->assertEquals('published', $node->moderation_state->value);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests that entities with special languages can be moderated.
 | 
			
		||||
   *
 | 
			
		||||
   * @dataProvider moderationWithSpecialLanguagesTestCases
 | 
			
		||||
   */
 | 
			
		||||
  public function testModerationWithSpecialLanguages($original_language, $updated_language): void {
 | 
			
		||||
    $workflow = $this->createEditorialWorkflow();
 | 
			
		||||
    $this->addEntityTypeAndBundleToWorkflow($workflow, $this->revEntityTypeId, $this->revEntityTypeId);
 | 
			
		||||
 | 
			
		||||
    // Create a test entity.
 | 
			
		||||
    $storage = $this->entityTypeManager->getStorage($this->revEntityTypeId);
 | 
			
		||||
    $entity = $storage->create([
 | 
			
		||||
      'langcode' => $original_language,
 | 
			
		||||
    ]);
 | 
			
		||||
    $entity->save();
 | 
			
		||||
    $this->assertEquals('draft', $entity->moderation_state->value);
 | 
			
		||||
 | 
			
		||||
    $entity->moderation_state->value = 'published';
 | 
			
		||||
    $entity->langcode = $updated_language;
 | 
			
		||||
    $entity->save();
 | 
			
		||||
 | 
			
		||||
    $this->assertEquals('published', $storage->load($entity->id())->moderation_state->value);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Test cases for ::testModerationWithSpecialLanguages().
 | 
			
		||||
   */
 | 
			
		||||
  public static function moderationWithSpecialLanguagesTestCases() {
 | 
			
		||||
    return [
 | 
			
		||||
      'Not specified to not specified' => [
 | 
			
		||||
        LanguageInterface::LANGCODE_NOT_SPECIFIED,
 | 
			
		||||
        LanguageInterface::LANGCODE_NOT_SPECIFIED,
 | 
			
		||||
      ],
 | 
			
		||||
      'English to not specified' => [
 | 
			
		||||
        'en',
 | 
			
		||||
        LanguageInterface::LANGCODE_NOT_SPECIFIED,
 | 
			
		||||
      ],
 | 
			
		||||
      'Not specified to english' => [
 | 
			
		||||
        LanguageInterface::LANGCODE_NOT_SPECIFIED,
 | 
			
		||||
        'en',
 | 
			
		||||
      ],
 | 
			
		||||
    ];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests changing the language of content without adding a translation.
 | 
			
		||||
   */
 | 
			
		||||
  public function testChangingContentLangcode(): void {
 | 
			
		||||
    $this->createContentType([
 | 
			
		||||
      'type' => 'test_type',
 | 
			
		||||
    ]);
 | 
			
		||||
    $workflow = $this->createEditorialWorkflow();
 | 
			
		||||
    $this->addEntityTypeAndBundleToWorkflow($workflow, 'node', 'test_type');
 | 
			
		||||
 | 
			
		||||
    $entity = Node::create([
 | 
			
		||||
      'title' => 'Test node',
 | 
			
		||||
      'langcode' => 'en',
 | 
			
		||||
      'type' => 'test_type',
 | 
			
		||||
    ]);
 | 
			
		||||
    $entity->save();
 | 
			
		||||
 | 
			
		||||
    $content_moderation_state = ContentModerationState::loadFromModeratedEntity($entity);
 | 
			
		||||
    $this->assertCount(1, $entity->getTranslationLanguages());
 | 
			
		||||
    $this->assertCount(1, $content_moderation_state->getTranslationLanguages());
 | 
			
		||||
    $this->assertEquals('en', $entity->langcode->value);
 | 
			
		||||
    $this->assertEquals('en', $content_moderation_state->langcode->value);
 | 
			
		||||
 | 
			
		||||
    $entity->langcode = 'fr';
 | 
			
		||||
    $entity->save();
 | 
			
		||||
 | 
			
		||||
    $content_moderation_state = ContentModerationState::loadFromModeratedEntity($entity);
 | 
			
		||||
    $this->assertCount(1, $entity->getTranslationLanguages());
 | 
			
		||||
    $this->assertCount(1, $content_moderation_state->getTranslationLanguages());
 | 
			
		||||
    $this->assertEquals('fr', $entity->langcode->value);
 | 
			
		||||
    $this->assertEquals('fr', $content_moderation_state->langcode->value);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests that a non-translatable entity type with a langcode can be moderated.
 | 
			
		||||
   */
 | 
			
		||||
  public function testNonTranslatableEntityTypeModeration(): void {
 | 
			
		||||
    $workflow = $this->createEditorialWorkflow();
 | 
			
		||||
    $this->addEntityTypeAndBundleToWorkflow($workflow, $this->revEntityTypeId, $this->revEntityTypeId);
 | 
			
		||||
 | 
			
		||||
    // Check that the tested entity type is not translatable.
 | 
			
		||||
    $entity_type = $this->entityTypeManager->getDefinition($this->revEntityTypeId);
 | 
			
		||||
    $this->assertFalse($entity_type->isTranslatable(), 'The test entity type is not translatable.');
 | 
			
		||||
 | 
			
		||||
    // Create a test entity.
 | 
			
		||||
    $storage = $this->entityTypeManager->getStorage($this->revEntityTypeId);
 | 
			
		||||
    $entity = $storage->create();
 | 
			
		||||
    $entity->save();
 | 
			
		||||
    $this->assertEquals('draft', $entity->moderation_state->value);
 | 
			
		||||
 | 
			
		||||
    $entity->moderation_state->value = 'published';
 | 
			
		||||
    $entity->save();
 | 
			
		||||
 | 
			
		||||
    $this->assertEquals('published', $storage->load($entity->id())->moderation_state->value);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests moderation of a non-translatable entity type with no langcode.
 | 
			
		||||
   */
 | 
			
		||||
  public function testNonLangcodeEntityTypeModeration(): void {
 | 
			
		||||
    // Unset the langcode entity key for 'entity_test_rev'.
 | 
			
		||||
    $entity_type = clone $this->entityTypeManager->getDefinition($this->revEntityTypeId);
 | 
			
		||||
    $keys = $entity_type->getKeys();
 | 
			
		||||
    unset($keys['langcode']);
 | 
			
		||||
    $entity_type->set('entity_keys', $keys);
 | 
			
		||||
    \Drupal::state()->set($this->revEntityTypeId . '.entity_type', $entity_type);
 | 
			
		||||
 | 
			
		||||
    // Update the entity type in order to remove the 'langcode' field.
 | 
			
		||||
    \Drupal::entityDefinitionUpdateManager()->updateFieldableEntityType($entity_type, \Drupal::service('entity_field.manager')->getFieldStorageDefinitions($entity_type->id()));
 | 
			
		||||
 | 
			
		||||
    $workflow = $this->createEditorialWorkflow();
 | 
			
		||||
    $this->addEntityTypeAndBundleToWorkflow($workflow, $this->revEntityTypeId, $this->revEntityTypeId);
 | 
			
		||||
 | 
			
		||||
    // Check that the tested entity type is not translatable and does not have a
 | 
			
		||||
    // 'langcode' entity key.
 | 
			
		||||
    $entity_type = $this->entityTypeManager->getDefinition($this->revEntityTypeId);
 | 
			
		||||
    $this->assertFalse($entity_type->isTranslatable(), 'The test entity type is not translatable.');
 | 
			
		||||
    $this->assertFalse($entity_type->getKey('langcode'), "The test entity type does not have a 'langcode' entity key.");
 | 
			
		||||
 | 
			
		||||
    // Create a test entity.
 | 
			
		||||
    $storage = $this->entityTypeManager->getStorage($this->revEntityTypeId);
 | 
			
		||||
    $entity = $storage->create();
 | 
			
		||||
    $entity->save();
 | 
			
		||||
    $this->assertEquals('draft', $entity->moderation_state->value);
 | 
			
		||||
 | 
			
		||||
    $entity->moderation_state->value = 'published';
 | 
			
		||||
    $entity->save();
 | 
			
		||||
 | 
			
		||||
    $this->assertEquals('published', $storage->load($entity->id())->moderation_state->value);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the dependencies of the workflow when using content moderation.
 | 
			
		||||
   */
 | 
			
		||||
  public function testWorkflowDependencies(): void {
 | 
			
		||||
    $node_type = $this->createContentType([
 | 
			
		||||
      'type' => 'example',
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    $workflow = $this->createEditorialWorkflow();
 | 
			
		||||
    // Test both a config and non-config based bundle and entity type.
 | 
			
		||||
    $this->addEntityTypeAndBundleToWorkflow($workflow, 'node', 'example');
 | 
			
		||||
    $this->addEntityTypeAndBundleToWorkflow($workflow, 'entity_test_rev', 'entity_test_rev');
 | 
			
		||||
    $this->addEntityTypeAndBundleToWorkflow($workflow, 'entity_test_no_bundle', 'entity_test_no_bundle');
 | 
			
		||||
 | 
			
		||||
    $this->assertEquals([
 | 
			
		||||
      'module' => [
 | 
			
		||||
        'content_moderation',
 | 
			
		||||
        'entity_test',
 | 
			
		||||
      ],
 | 
			
		||||
      'config' => [
 | 
			
		||||
        'node.type.example',
 | 
			
		||||
      ],
 | 
			
		||||
    ], $workflow->getDependencies());
 | 
			
		||||
 | 
			
		||||
    $this->assertEquals([
 | 
			
		||||
      'entity_test_no_bundle',
 | 
			
		||||
      'entity_test_rev',
 | 
			
		||||
      'node',
 | 
			
		||||
    ], $workflow->getTypePlugin()->getEntityTypes());
 | 
			
		||||
 | 
			
		||||
    // Delete the node type and ensure it is removed from the workflow.
 | 
			
		||||
    $node_type->delete();
 | 
			
		||||
    $workflow = Workflow::load('editorial');
 | 
			
		||||
    $entity_types = $workflow->getTypePlugin()->getEntityTypes();
 | 
			
		||||
    $this->assertNotContains('node', $entity_types);
 | 
			
		||||
 | 
			
		||||
    // Uninstall entity test and ensure it's removed from the workflow.
 | 
			
		||||
    $this->container->get('config.manager')->uninstall('module', 'entity_test');
 | 
			
		||||
    $workflow = Workflow::load('editorial');
 | 
			
		||||
    $entity_types = $workflow->getTypePlugin()->getEntityTypes();
 | 
			
		||||
    $this->assertEquals([], $entity_types);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the content moderation workflow dependencies for non-config bundles.
 | 
			
		||||
   */
 | 
			
		||||
  public function testWorkflowNonConfigBundleDependencies(): void {
 | 
			
		||||
    // Create a bundle not based on any particular configuration.
 | 
			
		||||
    EntityTestHelper::createBundle('test_bundle');
 | 
			
		||||
 | 
			
		||||
    $workflow = $this->createEditorialWorkflow();
 | 
			
		||||
    $workflow->getTypePlugin()->addEntityTypeAndBundle('entity_test', 'test_bundle');
 | 
			
		||||
    $workflow->save();
 | 
			
		||||
 | 
			
		||||
    // Ensure the bundle is correctly added to the workflow.
 | 
			
		||||
    $this->assertEquals([
 | 
			
		||||
      'module' => [
 | 
			
		||||
        'content_moderation',
 | 
			
		||||
        'entity_test',
 | 
			
		||||
      ],
 | 
			
		||||
    ], $workflow->getDependencies());
 | 
			
		||||
    $this->assertEquals([
 | 
			
		||||
      'test_bundle',
 | 
			
		||||
    ], $workflow->getTypePlugin()->getBundlesForEntityType('entity_test'));
 | 
			
		||||
 | 
			
		||||
    // Delete the test bundle to ensure the workflow entity responds
 | 
			
		||||
    // appropriately.
 | 
			
		||||
    EntityTestHelper::deleteBundle('test_bundle');
 | 
			
		||||
 | 
			
		||||
    $workflow = Workflow::load('editorial');
 | 
			
		||||
    $this->assertEquals([], $workflow->getTypePlugin()->getBundlesForEntityType('entity_test'));
 | 
			
		||||
    $this->assertEquals([
 | 
			
		||||
      'module' => [
 | 
			
		||||
        'content_moderation',
 | 
			
		||||
      ],
 | 
			
		||||
    ], $workflow->getDependencies());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the revision default state of the moderation state entity revisions.
 | 
			
		||||
   */
 | 
			
		||||
  public function testRevisionDefaultState(): void {
 | 
			
		||||
    foreach (static::basicModerationTestCases() as $case) {
 | 
			
		||||
      [$entity_type_id] = $case;
 | 
			
		||||
      $this->doTestRevisionDefaultState($entity_type_id);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the revision default state of the moderation state entity revisions.
 | 
			
		||||
   *
 | 
			
		||||
   * @param string $entity_type_id
 | 
			
		||||
   *   The ID of entity type to be tested.
 | 
			
		||||
   */
 | 
			
		||||
  public function doTestRevisionDefaultState($entity_type_id): void {
 | 
			
		||||
    // Check that the revision default state of the moderated entity and the
 | 
			
		||||
    // content moderation state entity always match.
 | 
			
		||||
    $entity = $this->createEntity($entity_type_id, 'published');
 | 
			
		||||
    $cms_entity = ContentModerationState::loadFromModeratedEntity($entity);
 | 
			
		||||
    $this->assertEquals($entity->isDefaultRevision(), $cms_entity->isDefaultRevision());
 | 
			
		||||
 | 
			
		||||
    $entity->get('moderation_state')->value = 'published';
 | 
			
		||||
    $entity->save();
 | 
			
		||||
    $cms_entity = ContentModerationState::loadFromModeratedEntity($entity);
 | 
			
		||||
    $this->assertEquals($entity->isDefaultRevision(), $cms_entity->isDefaultRevision());
 | 
			
		||||
 | 
			
		||||
    $entity->get('moderation_state')->value = 'draft';
 | 
			
		||||
    $entity->save();
 | 
			
		||||
    $cms_entity = ContentModerationState::loadFromModeratedEntity($entity);
 | 
			
		||||
    $this->assertEquals($entity->isDefaultRevision(), $cms_entity->isDefaultRevision());
 | 
			
		||||
 | 
			
		||||
    $entity->get('moderation_state')->value = 'published';
 | 
			
		||||
    $entity->save();
 | 
			
		||||
    $cms_entity = ContentModerationState::loadFromModeratedEntity($entity);
 | 
			
		||||
    $this->assertEquals($entity->isDefaultRevision(), $cms_entity->isDefaultRevision());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Creates an entity.
 | 
			
		||||
   *
 | 
			
		||||
   * The entity will have required fields populated and the corresponding bundle
 | 
			
		||||
   * will be enabled for content moderation.
 | 
			
		||||
   *
 | 
			
		||||
   * @param string $entity_type_id
 | 
			
		||||
   *   The entity type ID.
 | 
			
		||||
   * @param string $moderation_state
 | 
			
		||||
   *   (optional) The initial moderation state of the newly created entity.
 | 
			
		||||
   *   Defaults to 'published'.
 | 
			
		||||
   * @param bool $create_workflow
 | 
			
		||||
   *   (optional) Whether to create an editorial workflow and configure it for
 | 
			
		||||
   *   the given entity type. Defaults to TRUE.
 | 
			
		||||
   *
 | 
			
		||||
   * @return \Drupal\Core\Entity\ContentEntityInterface
 | 
			
		||||
   *   The created entity.
 | 
			
		||||
   */
 | 
			
		||||
  protected function createEntity($entity_type_id, $moderation_state = 'published', $create_workflow = TRUE) {
 | 
			
		||||
    $entity_type = $this->entityTypeManager->getDefinition($entity_type_id);
 | 
			
		||||
 | 
			
		||||
    $bundle_id = $entity_type_id;
 | 
			
		||||
    // Set up a bundle entity type for the specified entity type, if needed.
 | 
			
		||||
    if ($bundle_entity_type_id = $entity_type->getBundleEntityType()) {
 | 
			
		||||
      $bundle_entity_type = $this->entityTypeManager->getDefinition($bundle_entity_type_id);
 | 
			
		||||
      $bundle_entity_storage = $this->entityTypeManager->getStorage($bundle_entity_type_id);
 | 
			
		||||
 | 
			
		||||
      $bundle_id = 'example';
 | 
			
		||||
      if (!$bundle_entity_storage->load($bundle_id)) {
 | 
			
		||||
        $bundle_entity = $bundle_entity_storage->create([
 | 
			
		||||
          $bundle_entity_type->getKey('id') => 'example',
 | 
			
		||||
        ]);
 | 
			
		||||
        if ($bundle_entity_type->hasKey('label')) {
 | 
			
		||||
          $bundle_entity->set($bundle_entity_type->getKey('label'), $this->randomMachineName());
 | 
			
		||||
        }
 | 
			
		||||
        if ($entity_type_id == 'media') {
 | 
			
		||||
          $bundle_entity->set('source', 'test');
 | 
			
		||||
          $bundle_entity->save();
 | 
			
		||||
          $source_field = $bundle_entity->getSource()->createSourceField($bundle_entity);
 | 
			
		||||
          $source_field->getFieldStorageDefinition()->save();
 | 
			
		||||
          $source_field->save();
 | 
			
		||||
          $bundle_entity->set('source_configuration', [
 | 
			
		||||
            'source_field' => $source_field->getName(),
 | 
			
		||||
          ]);
 | 
			
		||||
        }
 | 
			
		||||
        $bundle_entity->save();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if ($create_workflow) {
 | 
			
		||||
      $workflow = $this->createEditorialWorkflow();
 | 
			
		||||
      $workflow->getTypePlugin()->addEntityTypeAndBundle($entity_type_id, $bundle_id);
 | 
			
		||||
      $workflow->save();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
 | 
			
		||||
    $entity_storage = $this->entityTypeManager->getStorage($entity_type_id);
 | 
			
		||||
    $entity = $entity_storage->create([
 | 
			
		||||
      $entity_type->getKey('label') => 'Test title',
 | 
			
		||||
      $entity_type->getKey('bundle') => $bundle_id,
 | 
			
		||||
      'moderation_state' => $moderation_state,
 | 
			
		||||
    ]);
 | 
			
		||||
    // Make sure we add values for all of the required fields.
 | 
			
		||||
    if ($entity_type_id == 'block_content') {
 | 
			
		||||
      $entity->info = $this->randomString();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    $entity->save();
 | 
			
		||||
    return $entity;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Reloads the entity after clearing the static cache.
 | 
			
		||||
   *
 | 
			
		||||
   * @param \Drupal\Core\Entity\EntityInterface $entity
 | 
			
		||||
   *   The entity to reload.
 | 
			
		||||
   * @param int|bool $revision_id
 | 
			
		||||
   *   The specific revision ID to load. Defaults FALSE and just loads the
 | 
			
		||||
   *   default revision.
 | 
			
		||||
   *
 | 
			
		||||
   * @return \Drupal\Core\Entity\EntityInterface
 | 
			
		||||
   *   The reloaded entity.
 | 
			
		||||
   */
 | 
			
		||||
  protected function reloadEntity(EntityInterface $entity, $revision_id = FALSE): EntityInterface {
 | 
			
		||||
    /** @var \Drupal\Core\Entity\RevisionableStorageInterface $storage */
 | 
			
		||||
    $storage = \Drupal::entityTypeManager()->getStorage($entity->getEntityTypeId());
 | 
			
		||||
    $storage->resetCache([$entity->id()]);
 | 
			
		||||
    if ($revision_id) {
 | 
			
		||||
      return $storage->loadRevision($revision_id);
 | 
			
		||||
    }
 | 
			
		||||
    return $storage->load($entity->id());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Checks the default revision ID and publishing status for an entity.
 | 
			
		||||
   *
 | 
			
		||||
   * @param \Drupal\Core\Entity\EntityInterface $entity
 | 
			
		||||
   *   An entity object.
 | 
			
		||||
   * @param int $revision_id
 | 
			
		||||
   *   The expected revision ID.
 | 
			
		||||
   * @param bool|null $published
 | 
			
		||||
   *   (optional) Whether to check if the entity is published or not. Defaults
 | 
			
		||||
   *   to TRUE.
 | 
			
		||||
   *
 | 
			
		||||
   * @internal
 | 
			
		||||
   */
 | 
			
		||||
  protected function assertDefaultRevision(EntityInterface $entity, int $revision_id, $published = TRUE): void {
 | 
			
		||||
    // Get the default revision.
 | 
			
		||||
    $entity = $this->reloadEntity($entity);
 | 
			
		||||
    $this->assertEquals($revision_id, $entity->getRevisionId());
 | 
			
		||||
 | 
			
		||||
    if ($published !== NULL && $entity instanceof EntityPublishedInterface) {
 | 
			
		||||
      $this->assertSame($published, $entity->isPublished());
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,208 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\content_moderation\Kernel;
 | 
			
		||||
 | 
			
		||||
use Drupal\entity_test\Entity\EntityTestMulRevPub;
 | 
			
		||||
use Drupal\KernelTests\KernelTestBase;
 | 
			
		||||
use Drupal\Tests\content_moderation\Traits\ContentModerationTestTrait;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Test content moderation when an entity is marked as 'syncing'.
 | 
			
		||||
 *
 | 
			
		||||
 * @group content_moderation
 | 
			
		||||
 */
 | 
			
		||||
class ContentModerationSyncingTest extends KernelTestBase {
 | 
			
		||||
 | 
			
		||||
  use ContentModerationTestTrait;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = [
 | 
			
		||||
    'user',
 | 
			
		||||
    'workflows',
 | 
			
		||||
    'content_moderation',
 | 
			
		||||
    'entity_test',
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
    $this->installEntitySchema('workflow');
 | 
			
		||||
    $this->installEntitySchema('content_moderation_state');
 | 
			
		||||
    $this->installEntitySchema('entity_test_mulrevpub');
 | 
			
		||||
 | 
			
		||||
    $workflow = $this->createEditorialWorkflow();
 | 
			
		||||
    $workflow->getTypePlugin()->addEntityTypeAndBundle('entity_test_mulrevpub', 'entity_test_mulrevpub');
 | 
			
		||||
    $workflow->save();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests no new revision is forced during a sync.
 | 
			
		||||
   */
 | 
			
		||||
  public function testNoRevisionForcedDuringSync(): void {
 | 
			
		||||
    $entity = EntityTestMulRevPub::create([
 | 
			
		||||
      'moderation_state' => 'draft',
 | 
			
		||||
      'name' => 'foo',
 | 
			
		||||
    ]);
 | 
			
		||||
    $entity->save();
 | 
			
		||||
    $initial_revision_id = $entity->getRevisionId();
 | 
			
		||||
 | 
			
		||||
    $entity->setSyncing(TRUE);
 | 
			
		||||
    $entity->name = 'bar';
 | 
			
		||||
    $entity->save();
 | 
			
		||||
 | 
			
		||||
    $this->assertEquals($entity->getRevisionId(), $initial_revision_id);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests changing the moderation state during a sync.
 | 
			
		||||
   */
 | 
			
		||||
  public function testSingleRevisionStateChangedDuringSync(): void {
 | 
			
		||||
    $entity = EntityTestMulRevPub::create([
 | 
			
		||||
      'moderation_state' => 'published',
 | 
			
		||||
      'name' => 'foo',
 | 
			
		||||
    ]);
 | 
			
		||||
    $entity->save();
 | 
			
		||||
    $initial_revision_id = $entity->getRevisionId();
 | 
			
		||||
    $this->assertTrue($entity->isDefaultRevision());
 | 
			
		||||
    $this->assertTrue($entity->isPublished());
 | 
			
		||||
 | 
			
		||||
    $entity->setSyncing(TRUE);
 | 
			
		||||
    $entity->moderation_state = 'draft';
 | 
			
		||||
    $entity->save();
 | 
			
		||||
 | 
			
		||||
    // If a moderation state is changed to a draft while syncing, it will revert
 | 
			
		||||
    // to the same properties of an item of content that was initially created
 | 
			
		||||
    // as a draft.
 | 
			
		||||
    $this->assertEquals($initial_revision_id, $entity->getRevisionId());
 | 
			
		||||
    $this->assertFalse($entity->isPublished());
 | 
			
		||||
    $this->assertTrue($entity->isDefaultRevision());
 | 
			
		||||
    $this->assertEquals('draft', $entity->moderation_state->value);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests state changes with multiple revisions during a sync.
 | 
			
		||||
   */
 | 
			
		||||
  public function testMultipleRevisionStateChangedDuringSync(): void {
 | 
			
		||||
    $entity = EntityTestMulRevPub::create([
 | 
			
		||||
      'moderation_state' => 'published',
 | 
			
		||||
      'name' => 'foo',
 | 
			
		||||
    ]);
 | 
			
		||||
    $entity->save();
 | 
			
		||||
 | 
			
		||||
    $entity->name = 'bar';
 | 
			
		||||
    $entity->save();
 | 
			
		||||
    $latest_revision_id = $entity->getRevisionId();
 | 
			
		||||
 | 
			
		||||
    $entity->setSyncing(TRUE);
 | 
			
		||||
    $entity->moderation_state = 'draft';
 | 
			
		||||
    $entity->save();
 | 
			
		||||
 | 
			
		||||
    $this->assertEquals($latest_revision_id, $entity->getRevisionId());
 | 
			
		||||
    $this->assertEquals('draft', $entity->moderation_state->value);
 | 
			
		||||
    $this->assertEquals('bar', $entity->name->value);
 | 
			
		||||
    // The default revision will not automatically be assigned to another
 | 
			
		||||
    // revision, so a draft unpublished revision will be created when syncing
 | 
			
		||||
    // 'published' to 'draft'.
 | 
			
		||||
    $this->assertFalse($entity->isPublished());
 | 
			
		||||
    $this->assertTrue($entity->isDefaultRevision());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests modifying a previous revision during a sync.
 | 
			
		||||
   */
 | 
			
		||||
  public function testUpdatingPreviousRevisionDuringSync(): void {
 | 
			
		||||
    /** @var \Drupal\Core\Entity\RevisionableStorageInterface $storage */
 | 
			
		||||
    $storage = $this->container->get('entity_type.manager')->getStorage('entity_test_mulrevpub');
 | 
			
		||||
 | 
			
		||||
    $entity = EntityTestMulRevPub::create([
 | 
			
		||||
      'moderation_state' => 'published',
 | 
			
		||||
      'name' => 'foo',
 | 
			
		||||
    ]);
 | 
			
		||||
    $entity->save();
 | 
			
		||||
    $original_revision_id = $entity->getRevisionId();
 | 
			
		||||
 | 
			
		||||
    $entity->name = 'bar';
 | 
			
		||||
    $entity->save();
 | 
			
		||||
 | 
			
		||||
    // Sync a change to the 'name' on the original revision ID.
 | 
			
		||||
    $original_revision = $storage->loadRevision($original_revision_id);
 | 
			
		||||
    $original_revision->setSyncing(TRUE);
 | 
			
		||||
    $original_revision->name = 'baz';
 | 
			
		||||
    $original_revision->save();
 | 
			
		||||
 | 
			
		||||
    // The names of each revision should reflect two revisions, the original one
 | 
			
		||||
    // having been updated during a sync.
 | 
			
		||||
    $this->assertEquals(['baz', 'bar'], $this->getAllRevisionNames($entity));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests a moderation state changed on a previous revision during a sync.
 | 
			
		||||
   */
 | 
			
		||||
  public function testStateChangedPreviousRevisionDuringSync(): void {
 | 
			
		||||
    /** @var \Drupal\Core\Entity\RevisionableStorageInterface $storage */
 | 
			
		||||
    $storage = $this->container->get('entity_type.manager')->getStorage('entity_test_mulrevpub');
 | 
			
		||||
 | 
			
		||||
    $entity = EntityTestMulRevPub::create([
 | 
			
		||||
      'moderation_state' => 'published',
 | 
			
		||||
      'name' => 'foo',
 | 
			
		||||
    ]);
 | 
			
		||||
    $entity->save();
 | 
			
		||||
 | 
			
		||||
    $entity->moderation_state = 'draft';
 | 
			
		||||
    $entity->name = 'bar';
 | 
			
		||||
    $entity->save();
 | 
			
		||||
    $draft_revision_id = $entity->getRevisionId();
 | 
			
		||||
 | 
			
		||||
    $entity->name = 'baz';
 | 
			
		||||
    $entity->moderation_state = 'published';
 | 
			
		||||
    $entity->save();
 | 
			
		||||
    $default_revision_id = $entity->getRevisionId();
 | 
			
		||||
 | 
			
		||||
    // Update the draft revision moderation state to published, which would
 | 
			
		||||
    // typically change the default status of a revision during moderation.
 | 
			
		||||
    $draft_revision = $storage->loadRevision($draft_revision_id);
 | 
			
		||||
    $draft_revision->setSyncing(TRUE);
 | 
			
		||||
    $draft_revision->name = 'qux';
 | 
			
		||||
    $draft_revision->moderation_state = 'published';
 | 
			
		||||
    $draft_revision->save();
 | 
			
		||||
 | 
			
		||||
    // Ensure the default revision is not changed during the sync.
 | 
			
		||||
    $reloaded_default_revision = $storage->load($entity->id());
 | 
			
		||||
    $this->assertEquals($default_revision_id, $reloaded_default_revision->getRevisionId());
 | 
			
		||||
    $this->assertEquals([
 | 
			
		||||
      'foo',
 | 
			
		||||
      'qux',
 | 
			
		||||
      'baz',
 | 
			
		||||
    ], $this->getAllRevisionNames($reloaded_default_revision));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Get all the revision names in order of the revision ID.
 | 
			
		||||
   *
 | 
			
		||||
   * @param \Drupal\entity_test\Entity\EntityTestMulRevPub $entity
 | 
			
		||||
   *   The entity.
 | 
			
		||||
   *
 | 
			
		||||
   * @return array
 | 
			
		||||
   *   An array of revision names.
 | 
			
		||||
   */
 | 
			
		||||
  protected function getAllRevisionNames(EntityTestMulRevPub $entity): array {
 | 
			
		||||
    /** @var \Drupal\Core\Entity\RevisionableStorageInterface $storage */
 | 
			
		||||
    $storage = $this->container->get('entity_type.manager')->getStorage('entity_test_mulrevpub');
 | 
			
		||||
    return array_map(function ($revision_id) use ($storage) {
 | 
			
		||||
      return $storage->loadRevision($revision_id)->name->value;
 | 
			
		||||
    }, array_keys($storage->getQuery()
 | 
			
		||||
      ->accessCheck(FALSE)
 | 
			
		||||
      ->allRevisions()
 | 
			
		||||
      ->condition('id', $entity->id())
 | 
			
		||||
      ->sort('revision_id', 'ASC')
 | 
			
		||||
      ->execute())
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,149 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\content_moderation\Kernel;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Config\ConfigImporterException;
 | 
			
		||||
use Drupal\KernelTests\KernelTestBase;
 | 
			
		||||
use Drupal\node\Entity\Node;
 | 
			
		||||
use Drupal\node\Entity\NodeType;
 | 
			
		||||
use Drupal\Tests\content_moderation\Traits\ContentModerationTestTrait;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests how Content Moderation handles workflow config changes.
 | 
			
		||||
 *
 | 
			
		||||
 * @group content_moderation
 | 
			
		||||
 */
 | 
			
		||||
class ContentModerationWorkflowConfigTest extends KernelTestBase {
 | 
			
		||||
 | 
			
		||||
  use ContentModerationTestTrait;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = [
 | 
			
		||||
    'node',
 | 
			
		||||
    'content_moderation',
 | 
			
		||||
    'user',
 | 
			
		||||
    'system',
 | 
			
		||||
    'text',
 | 
			
		||||
    'workflows',
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @var \Drupal\Core\Entity\EntityTypeManager
 | 
			
		||||
   */
 | 
			
		||||
  protected $entityTypeManager;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @var \Drupal\Core\Config\ConfigFactoryInterface
 | 
			
		||||
   */
 | 
			
		||||
  protected $configFactory;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @var \Drupal\workflows\Entity\Workflow
 | 
			
		||||
   */
 | 
			
		||||
  protected $workflow;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @var \Drupal\Core\Config\Entity\ConfigEntityStorage
 | 
			
		||||
   */
 | 
			
		||||
  protected $workflowStorage;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
 | 
			
		||||
    $this->installSchema('node', 'node_access');
 | 
			
		||||
    $this->installEntitySchema('node');
 | 
			
		||||
    $this->installEntitySchema('user');
 | 
			
		||||
    $this->installEntitySchema('content_moderation_state');
 | 
			
		||||
    $this->installConfig(['system', 'content_moderation']);
 | 
			
		||||
 | 
			
		||||
    NodeType::create([
 | 
			
		||||
      'type' => 'example',
 | 
			
		||||
      'name' => 'Example',
 | 
			
		||||
    ])->save();
 | 
			
		||||
 | 
			
		||||
    $workflow = $this->createEditorialWorkflow();
 | 
			
		||||
    $workflow->getTypePlugin()
 | 
			
		||||
      ->addState('test1', 'Test one')
 | 
			
		||||
      ->addState('test2', 'Test two')
 | 
			
		||||
      ->addState('test3', 'Test three')
 | 
			
		||||
      ->addEntityTypeAndBundle('node', 'example');
 | 
			
		||||
    $workflow->save();
 | 
			
		||||
    $this->workflow = $workflow;
 | 
			
		||||
 | 
			
		||||
    $this->copyConfig($this->container->get('config.storage'), $this->container->get('config.storage.sync'));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests deleting a state via config import.
 | 
			
		||||
   */
 | 
			
		||||
  public function testDeletingStateViaConfiguration(): void {
 | 
			
		||||
    $config_sync = \Drupal::service('config.storage.sync');
 | 
			
		||||
 | 
			
		||||
    // Alter the workflow data.
 | 
			
		||||
    $config_data = $this->config('workflows.workflow.editorial')->get();
 | 
			
		||||
    unset($config_data['type_settings']['states']['test1']);
 | 
			
		||||
    $config_sync->write('workflows.workflow.editorial', $config_data);
 | 
			
		||||
 | 
			
		||||
    // Alter the data of another entity type.
 | 
			
		||||
    $config_data = $this->config('node.type.example')->get();
 | 
			
		||||
    $config_data['description'] = 'A new description';
 | 
			
		||||
    $config_sync->write('node.type.example', $config_data);
 | 
			
		||||
 | 
			
		||||
    // Alter the values of simple config.
 | 
			
		||||
    $config_data = $this->config('core.extension')->get();
 | 
			
		||||
    $config_data['module']['node'] = 1;
 | 
			
		||||
    $config_sync->write('core.extension', $config_data);
 | 
			
		||||
 | 
			
		||||
    // There are no Nodes with the moderation state test1, so this should run
 | 
			
		||||
    // with no errors.
 | 
			
		||||
    $this->configImporter()->reset()->import();
 | 
			
		||||
 | 
			
		||||
    $node = Node::create([
 | 
			
		||||
      'type' => 'example',
 | 
			
		||||
      'title' => 'Test title',
 | 
			
		||||
      'moderation_state' => 'test2',
 | 
			
		||||
    ]);
 | 
			
		||||
    $node->save();
 | 
			
		||||
 | 
			
		||||
    $config_data = $this->config('workflows.workflow.editorial')->get();
 | 
			
		||||
    unset($config_data['type_settings']['states']['test2']);
 | 
			
		||||
    unset($config_data['type_settings']['states']['test3']);
 | 
			
		||||
    \Drupal::service('config.storage.sync')->write('workflows.workflow.editorial', $config_data);
 | 
			
		||||
 | 
			
		||||
    // Now there is a Node with the moderation state test2, this will fail.
 | 
			
		||||
    try {
 | 
			
		||||
      $this->configImporter()->reset()->import();
 | 
			
		||||
      $this->fail('ConfigImporterException not thrown, invalid import was not stopped due to deleted state.');
 | 
			
		||||
    }
 | 
			
		||||
    catch (ConfigImporterException $e) {
 | 
			
		||||
      $this->assertEquals('There were errors validating the config synchronization.' . PHP_EOL . 'The moderation state Test two is being used, but is not in the source storage.', $e->getMessage());
 | 
			
		||||
      $error_log = $this->configImporter->getErrors();
 | 
			
		||||
      $expected = ['The moderation state Test two is being used, but is not in the source storage.'];
 | 
			
		||||
      $this->assertEquals($expected, $error_log);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    \Drupal::service('config.storage.sync')->delete('workflows.workflow.editorial');
 | 
			
		||||
 | 
			
		||||
    // An error should be thrown when trying to delete an in use workflow.
 | 
			
		||||
    try {
 | 
			
		||||
      $this->configImporter()->reset()->import();
 | 
			
		||||
      $this->fail('ConfigImporterException not thrown, invalid import was not stopped due to deleted workflow.');
 | 
			
		||||
    }
 | 
			
		||||
    catch (ConfigImporterException $e) {
 | 
			
		||||
      $this->assertEquals('There were errors validating the config synchronization.' . PHP_EOL . 'The workflow Editorial is being used, and cannot be deleted.', $e->getMessage());
 | 
			
		||||
      $error_log = $this->configImporter->getErrors();
 | 
			
		||||
      $expected = [
 | 
			
		||||
        'The workflow Editorial is being used, and cannot be deleted.',
 | 
			
		||||
      ];
 | 
			
		||||
      $this->assertEquals($expected, $error_log);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,123 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\content_moderation\Kernel;
 | 
			
		||||
 | 
			
		||||
use Drupal\KernelTests\KernelTestBase;
 | 
			
		||||
use Drupal\workflows\Entity\Workflow;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests the API of the ContentModeration workflow type plugin.
 | 
			
		||||
 *
 | 
			
		||||
 * @group content_moderation
 | 
			
		||||
 *
 | 
			
		||||
 * @coversDefaultClass \Drupal\content_moderation\Plugin\WorkflowType\ContentModeration
 | 
			
		||||
 */
 | 
			
		||||
class ContentModerationWorkflowTypeApiTest extends KernelTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * A workflow for testing.
 | 
			
		||||
   *
 | 
			
		||||
   * @var \Drupal\workflows\Entity\Workflow
 | 
			
		||||
   */
 | 
			
		||||
  protected $workflow;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = [
 | 
			
		||||
    'workflows',
 | 
			
		||||
    'content_moderation',
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
    $this->workflow = Workflow::create(['id' => 'test', 'type' => 'content_moderation']);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @covers ::getBundlesForEntityType
 | 
			
		||||
   * @covers ::addEntityTypeAndBundle
 | 
			
		||||
   * @covers ::removeEntityTypeAndBundle
 | 
			
		||||
   */
 | 
			
		||||
  public function testGetBundlesForEntityType(): void {
 | 
			
		||||
    /** @var \Drupal\content_moderation\Plugin\WorkflowType\ContentModeration $workflow_plugin */
 | 
			
		||||
    $workflow_plugin = $this->workflow->getTypePlugin();
 | 
			
		||||
    // The content moderation plugin does not validate the existence of the
 | 
			
		||||
    // entity type or bundle.
 | 
			
		||||
    $this->assertEquals([], $workflow_plugin->getBundlesForEntityType('fake_node'));
 | 
			
		||||
    $workflow_plugin->addEntityTypeAndBundle('fake_node', 'fake_page');
 | 
			
		||||
    $this->assertEquals(['fake_page'], $workflow_plugin->getBundlesForEntityType('fake_node'));
 | 
			
		||||
    $this->assertEquals([], $workflow_plugin->getBundlesForEntityType('fake_block'));
 | 
			
		||||
    $workflow_plugin->removeEntityTypeAndBundle('fake_node', 'fake_page');
 | 
			
		||||
    $this->assertEquals([], $workflow_plugin->getBundlesForEntityType('fake_node'));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @covers ::appliesToEntityTypeAndBundle
 | 
			
		||||
   * @covers ::addEntityTypeAndBundle
 | 
			
		||||
   * @covers ::removeEntityTypeAndBundle
 | 
			
		||||
   */
 | 
			
		||||
  public function testAppliesToEntityTypeAndBundle(): void {
 | 
			
		||||
    /** @var \Drupal\content_moderation\Plugin\WorkflowType\ContentModeration $workflow_plugin */
 | 
			
		||||
    $workflow_plugin = $this->workflow->getTypePlugin();
 | 
			
		||||
    // The content moderation plugin does not validate the existence of the
 | 
			
		||||
    // entity type or bundle.
 | 
			
		||||
    $this->assertFalse($workflow_plugin->appliesToEntityTypeAndBundle('fake_node', 'fake_page'));
 | 
			
		||||
    $workflow_plugin->addEntityTypeAndBundle('fake_node', 'fake_page');
 | 
			
		||||
    $this->assertTrue($workflow_plugin->appliesToEntityTypeAndBundle('fake_node', 'fake_page'));
 | 
			
		||||
    $this->assertFalse($workflow_plugin->appliesToEntityTypeAndBundle('fake_block', 'fake_custom'));
 | 
			
		||||
    $workflow_plugin->removeEntityTypeAndBundle('fake_node', 'fake_page');
 | 
			
		||||
    $this->assertFalse($workflow_plugin->appliesToEntityTypeAndBundle('fake_node', 'fake_page'));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @covers ::addEntityTypeAndBundle
 | 
			
		||||
   */
 | 
			
		||||
  public function testAddEntityTypeAndBundle(): void {
 | 
			
		||||
    /** @var \Drupal\content_moderation\Plugin\WorkflowType\ContentModeration $workflow_plugin */
 | 
			
		||||
    $workflow_plugin = $this->workflow->getTypePlugin();
 | 
			
		||||
 | 
			
		||||
    // The bundles are intentionally added in reverse alphabetical order.
 | 
			
		||||
    $workflow_plugin->addEntityTypeAndBundle('fake_node', 'fake_page');
 | 
			
		||||
    $workflow_plugin->addEntityTypeAndBundle('fake_node', 'fake_article');
 | 
			
		||||
 | 
			
		||||
    // Add another entity type that comes alphabetically before 'fake_node'.
 | 
			
		||||
    $workflow_plugin->addEntityTypeAndBundle('fake_block', 'fake_custom');
 | 
			
		||||
 | 
			
		||||
    // The entity type keys and bundle values should be sorted alphabetically.
 | 
			
		||||
    // The bundle array index should not reflect the order in which they are
 | 
			
		||||
    // added.
 | 
			
		||||
    $this->assertSame(
 | 
			
		||||
      ['fake_block' => ['fake_custom'], 'fake_node' => ['fake_article', 'fake_page']],
 | 
			
		||||
      $workflow_plugin->getConfiguration()['entity_types']
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @covers ::addEntityTypeAndBundle
 | 
			
		||||
   * @covers ::removeEntityTypeAndBundle
 | 
			
		||||
   */
 | 
			
		||||
  public function testRemoveEntityTypeAndBundle(): void {
 | 
			
		||||
    /** @var \Drupal\content_moderation\Plugin\WorkflowType\ContentModeration $workflow_plugin */
 | 
			
		||||
    $workflow_plugin = $this->workflow->getTypePlugin();
 | 
			
		||||
 | 
			
		||||
    // There should be no bundles for fake_node to start with.
 | 
			
		||||
    $this->assertEquals([], $workflow_plugin->getBundlesForEntityType('fake_node'));
 | 
			
		||||
    // Removing a bundle which is not set on the workflow should not throw an
 | 
			
		||||
    // error and should still result in none being returned.
 | 
			
		||||
    $workflow_plugin->removeEntityTypeAndBundle('fake_node', 'fake_page');
 | 
			
		||||
    $this->assertEquals([], $workflow_plugin->getBundlesForEntityType('fake_node'));
 | 
			
		||||
    // Adding a bundle for fake_node should result it in being returned, but
 | 
			
		||||
    // then removing it will return no bundles for fake_node.
 | 
			
		||||
    $workflow_plugin->addEntityTypeAndBundle('fake_node', 'fake_page');
 | 
			
		||||
    $this->assertEquals(['fake_page'], $workflow_plugin->getBundlesForEntityType('fake_node'));
 | 
			
		||||
    $workflow_plugin->removeEntityTypeAndBundle('fake_node', 'fake_page');
 | 
			
		||||
    $this->assertEquals([], $workflow_plugin->getBundlesForEntityType('fake_node'));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,149 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\content_moderation\Kernel;
 | 
			
		||||
 | 
			
		||||
use Drupal\KernelTests\KernelTestBase;
 | 
			
		||||
use Drupal\language\Entity\ConfigurableLanguage;
 | 
			
		||||
use Drupal\node\Entity\Node;
 | 
			
		||||
use Drupal\node\Entity\NodeType;
 | 
			
		||||
use Drupal\Tests\content_moderation\Traits\ContentModerationTestTrait;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests the correct default revision is set.
 | 
			
		||||
 *
 | 
			
		||||
 * @group content_moderation
 | 
			
		||||
 */
 | 
			
		||||
class DefaultRevisionStateTest extends KernelTestBase {
 | 
			
		||||
 | 
			
		||||
  use ContentModerationTestTrait;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = [
 | 
			
		||||
    'entity_test',
 | 
			
		||||
    'node',
 | 
			
		||||
    'block_content',
 | 
			
		||||
    'content_moderation',
 | 
			
		||||
    'user',
 | 
			
		||||
    'system',
 | 
			
		||||
    'language',
 | 
			
		||||
    'content_translation',
 | 
			
		||||
    'text',
 | 
			
		||||
    'workflows',
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @var \Drupal\Core\Entity\EntityTypeManager
 | 
			
		||||
   */
 | 
			
		||||
  protected $entityTypeManager;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
 | 
			
		||||
    $this->installSchema('node', 'node_access');
 | 
			
		||||
    $this->installEntitySchema('node');
 | 
			
		||||
    $this->installEntitySchema('user');
 | 
			
		||||
    $this->installEntitySchema('entity_test_with_bundle');
 | 
			
		||||
    $this->installEntitySchema('entity_test_rev');
 | 
			
		||||
    $this->installEntitySchema('entity_test_mulrevpub');
 | 
			
		||||
    $this->installEntitySchema('block_content');
 | 
			
		||||
    $this->installEntitySchema('content_moderation_state');
 | 
			
		||||
    $this->installConfig('content_moderation');
 | 
			
		||||
 | 
			
		||||
    $this->entityTypeManager = $this->container->get('entity_type.manager');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests a translatable Node.
 | 
			
		||||
   */
 | 
			
		||||
  public function testMultilingual(): void {
 | 
			
		||||
    // Enable French.
 | 
			
		||||
    ConfigurableLanguage::createFromLangcode('fr')->save();
 | 
			
		||||
    $node_type = NodeType::create([
 | 
			
		||||
      'type' => 'example',
 | 
			
		||||
      'name' => 'Example',
 | 
			
		||||
    ]);
 | 
			
		||||
    $node_type->save();
 | 
			
		||||
 | 
			
		||||
    $this->container->get('content_translation.manager')->setEnabled('node', 'example', TRUE);
 | 
			
		||||
 | 
			
		||||
    $workflow = $this->createEditorialWorkflow();
 | 
			
		||||
    $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example');
 | 
			
		||||
    $workflow->save();
 | 
			
		||||
 | 
			
		||||
    $english_node = Node::create([
 | 
			
		||||
      'type' => 'example',
 | 
			
		||||
      'title' => 'Test title',
 | 
			
		||||
    ]);
 | 
			
		||||
    // Revision 1 (en).
 | 
			
		||||
    $english_node
 | 
			
		||||
      ->setUnpublished()
 | 
			
		||||
      ->save();
 | 
			
		||||
    $this->assertEquals('draft', $english_node->moderation_state->value);
 | 
			
		||||
    $this->assertFalse($english_node->isPublished());
 | 
			
		||||
    $this->assertTrue($english_node->isDefaultRevision());
 | 
			
		||||
    $this->assertModerationState($english_node->getRevisionId(), $english_node->language()->getId(), 'draft');
 | 
			
		||||
 | 
			
		||||
    // Revision 2 (fr)
 | 
			
		||||
    $french_node = $english_node->addTranslation('fr', ['title' => 'French title']);
 | 
			
		||||
    $french_node->moderation_state->value = 'published';
 | 
			
		||||
    $french_node->save();
 | 
			
		||||
    $this->assertTrue($french_node->isPublished());
 | 
			
		||||
    $this->assertTrue($french_node->isDefaultRevision());
 | 
			
		||||
    $this->assertModerationState($french_node->getRevisionId(), $french_node->language()->getId(), 'published');
 | 
			
		||||
 | 
			
		||||
    // Revision 3 (fr)
 | 
			
		||||
    $node = Node::load($english_node->id())->getTranslation('fr');
 | 
			
		||||
    $node->moderation_state->value = 'draft';
 | 
			
		||||
    $node->save();
 | 
			
		||||
    $this->assertFalse($node->isPublished());
 | 
			
		||||
    $this->assertFalse($node->isDefaultRevision());
 | 
			
		||||
    $this->assertModerationState($node->getRevisionId(), $node->language()->getId(), 'draft');
 | 
			
		||||
 | 
			
		||||
    // Revision 4 (en)
 | 
			
		||||
    $latest_revision = $this->entityTypeManager->getStorage('node')->loadRevision(3);
 | 
			
		||||
    $latest_revision->moderation_state->value = 'draft';
 | 
			
		||||
    $latest_revision->save();
 | 
			
		||||
    $this->assertFalse($latest_revision->isPublished());
 | 
			
		||||
    $this->assertFalse($latest_revision->isDefaultRevision());
 | 
			
		||||
    $this->assertModerationState($latest_revision->getRevisionId(), $latest_revision->language()->getId(), 'draft');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Verifies the expected moderation state revision exists.
 | 
			
		||||
   *
 | 
			
		||||
   * @param string $revision_id
 | 
			
		||||
   *   The revision ID of the host entity.
 | 
			
		||||
   * @param string $langcode
 | 
			
		||||
   *   The language code of the host entity to check.
 | 
			
		||||
   * @param string $expected_state
 | 
			
		||||
   *   The state the content moderation state revision should be in.
 | 
			
		||||
   * @param string $expected_workflow
 | 
			
		||||
   *   The workflow the content moderation state revision should be using.
 | 
			
		||||
   *
 | 
			
		||||
   * @internal
 | 
			
		||||
   */
 | 
			
		||||
  protected function assertModerationState(string $revision_id, string $langcode, string $expected_state, string $expected_workflow = 'editorial'): void {
 | 
			
		||||
    $moderation_state_storage = $this->entityTypeManager->getStorage('content_moderation_state');
 | 
			
		||||
 | 
			
		||||
    $query = $moderation_state_storage->getQuery()->accessCheck(FALSE);
 | 
			
		||||
    $results = $query->allRevisions()
 | 
			
		||||
      ->condition('content_entity_revision_id', $revision_id)
 | 
			
		||||
      ->condition('langcode', $langcode)
 | 
			
		||||
      ->execute();
 | 
			
		||||
    $this->assertCount(1, $results);
 | 
			
		||||
 | 
			
		||||
    $moderation_state = $moderation_state_storage
 | 
			
		||||
      ->loadRevision(key($results))
 | 
			
		||||
      ->getTranslation($langcode);
 | 
			
		||||
    $this->assertEquals($expected_state, $moderation_state->get('moderation_state')->value);
 | 
			
		||||
    $this->assertEquals($expected_workflow, $moderation_state->get('workflow')->target_id);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,186 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\content_moderation\Kernel;
 | 
			
		||||
 | 
			
		||||
use Drupal\KernelTests\KernelTestBase;
 | 
			
		||||
use Drupal\node\Entity\Node;
 | 
			
		||||
use Drupal\node\Entity\NodeType;
 | 
			
		||||
use Drupal\Tests\content_moderation\Traits\ContentModerationTestTrait;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @coversDefaultClass \Drupal\content_moderation\EntityOperations
 | 
			
		||||
 *
 | 
			
		||||
 * @group content_moderation
 | 
			
		||||
 */
 | 
			
		||||
class EntityOperationsTest extends KernelTestBase {
 | 
			
		||||
 | 
			
		||||
  use ContentModerationTestTrait;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = [
 | 
			
		||||
    'content_moderation',
 | 
			
		||||
    'node',
 | 
			
		||||
    'user',
 | 
			
		||||
    'system',
 | 
			
		||||
    'workflows',
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
    $this->installEntitySchema('node');
 | 
			
		||||
    $this->installSchema('node', 'node_access');
 | 
			
		||||
    $this->installEntitySchema('user');
 | 
			
		||||
    $this->installEntitySchema('content_moderation_state');
 | 
			
		||||
    $this->installConfig('content_moderation');
 | 
			
		||||
 | 
			
		||||
    $this->createNodeType();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Creates a page node type to test with, ensuring that it's moderated.
 | 
			
		||||
   */
 | 
			
		||||
  protected function createNodeType(): void {
 | 
			
		||||
    $node_type = NodeType::create([
 | 
			
		||||
      'type' => 'page',
 | 
			
		||||
      'name' => 'Page',
 | 
			
		||||
    ]);
 | 
			
		||||
    $node_type->save();
 | 
			
		||||
    $workflow = $this->createEditorialWorkflow();
 | 
			
		||||
    $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'page');
 | 
			
		||||
    $workflow->save();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Verifies that the process of saving pending revisions works as expected.
 | 
			
		||||
   */
 | 
			
		||||
  public function testPendingRevisions(): void {
 | 
			
		||||
    // Create a new node in draft.
 | 
			
		||||
    $page = Node::create([
 | 
			
		||||
      'type' => 'page',
 | 
			
		||||
      'title' => 'A',
 | 
			
		||||
    ]);
 | 
			
		||||
    $page->moderation_state->value = 'draft';
 | 
			
		||||
    $page->save();
 | 
			
		||||
 | 
			
		||||
    $id = $page->id();
 | 
			
		||||
 | 
			
		||||
    // Verify the entity saved correctly, and that the presence of pending
 | 
			
		||||
    // revisions doesn't affect the default node load.
 | 
			
		||||
    /** @var \Drupal\node\Entity\Node $page */
 | 
			
		||||
    $page = Node::load($id);
 | 
			
		||||
    $this->assertEquals('A', $page->getTitle());
 | 
			
		||||
    $this->assertTrue($page->isDefaultRevision());
 | 
			
		||||
    $this->assertFalse($page->isPublished());
 | 
			
		||||
 | 
			
		||||
    // Moderate the entity to published.
 | 
			
		||||
    $page->setTitle('B');
 | 
			
		||||
    $page->moderation_state->value = 'published';
 | 
			
		||||
    $page->save();
 | 
			
		||||
 | 
			
		||||
    // Verify the entity is now published and public.
 | 
			
		||||
    $page = Node::load($id);
 | 
			
		||||
    $this->assertEquals('B', $page->getTitle());
 | 
			
		||||
    $this->assertTrue($page->isDefaultRevision());
 | 
			
		||||
    $this->assertTrue($page->isPublished());
 | 
			
		||||
 | 
			
		||||
    // Make a new pending revision in Draft.
 | 
			
		||||
    $page->setTitle('C');
 | 
			
		||||
    $page->moderation_state->value = 'draft';
 | 
			
		||||
    $page->save();
 | 
			
		||||
 | 
			
		||||
    // Verify normal loads return the still-default previous version.
 | 
			
		||||
    $page = Node::load($id);
 | 
			
		||||
    $this->assertEquals('B', $page->getTitle());
 | 
			
		||||
 | 
			
		||||
    // Verify we can load the pending revision, even if the mechanism is kind
 | 
			
		||||
    // of gross. Note: revisionIds() is only available on NodeStorageInterface,
 | 
			
		||||
    // so this won't work for non-nodes. We'd need to use entity queries. This
 | 
			
		||||
    // is a core bug that should get fixed.
 | 
			
		||||
    $storage = \Drupal::entityTypeManager()->getStorage('node');
 | 
			
		||||
    $revision_ids = $storage->revisionIds($page);
 | 
			
		||||
    sort($revision_ids);
 | 
			
		||||
    $latest = end($revision_ids);
 | 
			
		||||
    $page = $storage->loadRevision($latest);
 | 
			
		||||
    $this->assertEquals('C', $page->getTitle());
 | 
			
		||||
 | 
			
		||||
    $page->setTitle('D');
 | 
			
		||||
    $page->moderation_state->value = 'published';
 | 
			
		||||
    $page->save();
 | 
			
		||||
 | 
			
		||||
    // Verify normal loads return the still-default previous version.
 | 
			
		||||
    $page = Node::load($id);
 | 
			
		||||
    $this->assertEquals('D', $page->getTitle());
 | 
			
		||||
    $this->assertTrue($page->isDefaultRevision());
 | 
			
		||||
    $this->assertTrue($page->isPublished());
 | 
			
		||||
 | 
			
		||||
    // Now check that we can immediately add a new published revision over it.
 | 
			
		||||
    $page->setTitle('E');
 | 
			
		||||
    $page->moderation_state->value = 'published';
 | 
			
		||||
    $page->save();
 | 
			
		||||
 | 
			
		||||
    $page = Node::load($id);
 | 
			
		||||
    $this->assertEquals('E', $page->getTitle());
 | 
			
		||||
    $this->assertTrue($page->isDefaultRevision());
 | 
			
		||||
    $this->assertTrue($page->isPublished());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Verifies that a newly-created node can go straight to published.
 | 
			
		||||
   */
 | 
			
		||||
  public function testPublishedCreation(): void {
 | 
			
		||||
    // Create a new node in draft.
 | 
			
		||||
    $page = Node::create([
 | 
			
		||||
      'type' => 'page',
 | 
			
		||||
      'title' => 'A',
 | 
			
		||||
    ]);
 | 
			
		||||
    $page->moderation_state->value = 'published';
 | 
			
		||||
    $page->save();
 | 
			
		||||
 | 
			
		||||
    $id = $page->id();
 | 
			
		||||
 | 
			
		||||
    // Verify the entity saved correctly.
 | 
			
		||||
    /** @var \Drupal\node\Entity\Node $page */
 | 
			
		||||
    $page = Node::load($id);
 | 
			
		||||
    $this->assertEquals('A', $page->getTitle());
 | 
			
		||||
    $this->assertTrue($page->isDefaultRevision());
 | 
			
		||||
    $this->assertTrue($page->isPublished());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Verifies that an unpublished state may be made the default revision.
 | 
			
		||||
   */
 | 
			
		||||
  public function testArchive(): void {
 | 
			
		||||
    $page = Node::create([
 | 
			
		||||
      'type' => 'page',
 | 
			
		||||
      'title' => $this->randomString(),
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    $page->moderation_state->value = 'published';
 | 
			
		||||
    $page->save();
 | 
			
		||||
 | 
			
		||||
    $id = $page->id();
 | 
			
		||||
 | 
			
		||||
    // The newly-created page should already be published.
 | 
			
		||||
    $page = Node::load($id);
 | 
			
		||||
    $this->assertTrue($page->isPublished());
 | 
			
		||||
 | 
			
		||||
    // When the page is moderated to the archived state, then the latest
 | 
			
		||||
    // revision should be the default revision, and it should be unpublished.
 | 
			
		||||
    $page->moderation_state->value = 'archived';
 | 
			
		||||
    $page->save();
 | 
			
		||||
    $new_revision_id = $page->getRevisionId();
 | 
			
		||||
 | 
			
		||||
    $storage = \Drupal::entityTypeManager()->getStorage('node');
 | 
			
		||||
    $new_revision = $storage->loadRevision($new_revision_id);
 | 
			
		||||
    $this->assertFalse($new_revision->isPublished());
 | 
			
		||||
    $this->assertTrue($new_revision->isDefaultRevision());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,413 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\content_moderation\Kernel;
 | 
			
		||||
 | 
			
		||||
use Drupal\KernelTests\KernelTestBase;
 | 
			
		||||
use Drupal\language\Entity\ConfigurableLanguage;
 | 
			
		||||
use Drupal\node\Entity\Node;
 | 
			
		||||
use Drupal\node\Entity\NodeType;
 | 
			
		||||
use Drupal\Tests\content_moderation\Traits\ContentModerationTestTrait;
 | 
			
		||||
use Drupal\Tests\user\Traits\UserCreationTrait;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @coversDefaultClass \Drupal\content_moderation\Plugin\Validation\Constraint\ModerationStateConstraintValidator
 | 
			
		||||
 * @group content_moderation
 | 
			
		||||
 */
 | 
			
		||||
class EntityStateChangeValidationTest extends KernelTestBase {
 | 
			
		||||
 | 
			
		||||
  use ContentModerationTestTrait;
 | 
			
		||||
  use UserCreationTrait;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = [
 | 
			
		||||
    'node',
 | 
			
		||||
    'content_moderation',
 | 
			
		||||
    'user',
 | 
			
		||||
    'system',
 | 
			
		||||
    'language',
 | 
			
		||||
    'content_translation',
 | 
			
		||||
    'workflows',
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * An admin user.
 | 
			
		||||
   *
 | 
			
		||||
   * @var \Drupal\Core\Session\AccountInterface
 | 
			
		||||
   */
 | 
			
		||||
  protected $adminUser;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
 | 
			
		||||
    $this->installSchema('node', 'node_access');
 | 
			
		||||
    $this->installEntitySchema('node');
 | 
			
		||||
    $this->installEntitySchema('user');
 | 
			
		||||
    $this->installEntitySchema('content_moderation_state');
 | 
			
		||||
    $this->installConfig('content_moderation');
 | 
			
		||||
 | 
			
		||||
    $this->adminUser = $this->createUser(array_keys($this->container->get('user.permissions')->getPermissions()));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests valid transitions.
 | 
			
		||||
   *
 | 
			
		||||
   * @covers ::validate
 | 
			
		||||
   */
 | 
			
		||||
  public function testValidTransition(): void {
 | 
			
		||||
 | 
			
		||||
    $node_type = NodeType::create([
 | 
			
		||||
      'type' => 'example',
 | 
			
		||||
      'name' => 'Example',
 | 
			
		||||
    ]);
 | 
			
		||||
    $node_type->save();
 | 
			
		||||
    $workflow = $this->createEditorialWorkflow();
 | 
			
		||||
    $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example');
 | 
			
		||||
    $workflow->save();
 | 
			
		||||
 | 
			
		||||
    $node = Node::create([
 | 
			
		||||
      'type' => 'example',
 | 
			
		||||
      'title' => 'Test title',
 | 
			
		||||
    ]);
 | 
			
		||||
    $node->moderation_state->value = 'draft';
 | 
			
		||||
    $node->save();
 | 
			
		||||
 | 
			
		||||
    $this->setCurrentUser($this->createUser(['use editorial transition publish']));
 | 
			
		||||
    $node->moderation_state->value = 'published';
 | 
			
		||||
    $this->assertCount(0, $node->validate());
 | 
			
		||||
    $node->save();
 | 
			
		||||
 | 
			
		||||
    $this->assertEquals('published', $node->moderation_state->value);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests invalid transitions.
 | 
			
		||||
   *
 | 
			
		||||
   * @covers ::validate
 | 
			
		||||
   */
 | 
			
		||||
  public function testInvalidTransition(): void {
 | 
			
		||||
    $this->setCurrentUser($this->adminUser);
 | 
			
		||||
 | 
			
		||||
    $node_type = NodeType::create([
 | 
			
		||||
      'type' => 'example',
 | 
			
		||||
      'name' => 'Example',
 | 
			
		||||
    ]);
 | 
			
		||||
    $node_type->save();
 | 
			
		||||
    $workflow = $this->createEditorialWorkflow();
 | 
			
		||||
    $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example');
 | 
			
		||||
    $workflow->save();
 | 
			
		||||
 | 
			
		||||
    $node = Node::create([
 | 
			
		||||
      'type' => 'example',
 | 
			
		||||
      'title' => 'Test title',
 | 
			
		||||
    ]);
 | 
			
		||||
    $node->moderation_state->value = 'draft';
 | 
			
		||||
    $node->save();
 | 
			
		||||
 | 
			
		||||
    $node->moderation_state->value = 'archived';
 | 
			
		||||
    $violations = $node->validate();
 | 
			
		||||
    $this->assertCount(1, $violations);
 | 
			
		||||
 | 
			
		||||
    $this->assertEquals('Invalid state transition from Draft to Archived', $violations->get(0)->getMessage());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests validation with an invalid state.
 | 
			
		||||
   */
 | 
			
		||||
  public function testInvalidState(): void {
 | 
			
		||||
    $node_type = NodeType::create([
 | 
			
		||||
      'type' => 'example',
 | 
			
		||||
      'name' => 'Example',
 | 
			
		||||
    ]);
 | 
			
		||||
    $node_type->save();
 | 
			
		||||
    $workflow = $this->createEditorialWorkflow();
 | 
			
		||||
    $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example');
 | 
			
		||||
    $workflow->save();
 | 
			
		||||
 | 
			
		||||
    $node = Node::create([
 | 
			
		||||
      'type' => 'example',
 | 
			
		||||
      'title' => 'Test title',
 | 
			
		||||
    ]);
 | 
			
		||||
    $node->moderation_state->value = 'invalid_state';
 | 
			
		||||
    $violations = $node->validate();
 | 
			
		||||
 | 
			
		||||
    $this->assertCount(1, $violations);
 | 
			
		||||
    $this->assertEquals('State invalid_state does not exist on Editorial workflow', $violations->get(0)->getMessage());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests validation with no initial state or an invalid state.
 | 
			
		||||
   */
 | 
			
		||||
  public function testInvalidStateWithoutExisting(): void {
 | 
			
		||||
    // Create content without moderation enabled for the content type.
 | 
			
		||||
    $node_type = NodeType::create([
 | 
			
		||||
      'type' => 'example',
 | 
			
		||||
      'name' => 'Example',
 | 
			
		||||
    ]);
 | 
			
		||||
    $node_type->save();
 | 
			
		||||
    $node = Node::create([
 | 
			
		||||
      'type' => 'example',
 | 
			
		||||
      'title' => 'Test title',
 | 
			
		||||
    ]);
 | 
			
		||||
    $node->save();
 | 
			
		||||
 | 
			
		||||
    // Enable moderation to test validation on existing content, with no
 | 
			
		||||
    // explicit state.
 | 
			
		||||
    $workflow = $this->createEditorialWorkflow();
 | 
			
		||||
    $workflow->getTypePlugin()->addState('deleted_state', 'Deleted state');
 | 
			
		||||
    $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example');
 | 
			
		||||
    $workflow->save();
 | 
			
		||||
 | 
			
		||||
    $this->setCurrentUser($this->createUser(['use editorial transition create_new_draft']));
 | 
			
		||||
    // Validate the invalid state.
 | 
			
		||||
    $node = Node::load($node->id());
 | 
			
		||||
    $node->moderation_state->value = 'invalid_state';
 | 
			
		||||
    $violations = $node->validate();
 | 
			
		||||
    $this->assertCount(1, $violations);
 | 
			
		||||
 | 
			
		||||
    // Assign the node to a state we're going to delete.
 | 
			
		||||
    $node->moderation_state->value = 'deleted_state';
 | 
			
		||||
    $node->save();
 | 
			
		||||
 | 
			
		||||
    // Delete the state so the original entity contains an invalid state when
 | 
			
		||||
    // validating.
 | 
			
		||||
    $workflow->getTypePlugin()->deleteState('deleted_state');
 | 
			
		||||
    $workflow->save();
 | 
			
		||||
 | 
			
		||||
    // When there is an invalid state, the content will revert to "draft". This
 | 
			
		||||
    // will allow a draft to draft transition.
 | 
			
		||||
    $node->moderation_state->value = 'draft';
 | 
			
		||||
    $violations = $node->validate();
 | 
			
		||||
    $this->assertCount(0, $violations);
 | 
			
		||||
    // This will disallow a draft to archived transition.
 | 
			
		||||
    $node->moderation_state->value = 'archived';
 | 
			
		||||
    $violations = $node->validate();
 | 
			
		||||
    $this->assertCount(1, $violations);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests state transition validation with multiple languages.
 | 
			
		||||
   */
 | 
			
		||||
  public function testInvalidStateMultilingual(): void {
 | 
			
		||||
 | 
			
		||||
    ConfigurableLanguage::createFromLangcode('fr')->save();
 | 
			
		||||
    $node_type = NodeType::create([
 | 
			
		||||
      'type' => 'example',
 | 
			
		||||
      'name' => 'Example',
 | 
			
		||||
    ]);
 | 
			
		||||
    $node_type->save();
 | 
			
		||||
 | 
			
		||||
    $workflow = $this->createEditorialWorkflow();
 | 
			
		||||
    $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example');
 | 
			
		||||
    $workflow->save();
 | 
			
		||||
 | 
			
		||||
    $this->setCurrentUser($this->createUser(['use editorial transition archive']));
 | 
			
		||||
    $node = Node::create([
 | 
			
		||||
      'type' => 'example',
 | 
			
		||||
      'title' => 'English Published Node',
 | 
			
		||||
      'langcode' => 'en',
 | 
			
		||||
      'moderation_state' => 'published',
 | 
			
		||||
    ]);
 | 
			
		||||
    $node->save();
 | 
			
		||||
 | 
			
		||||
    $node_fr = $node->addTranslation('fr', $node->toArray());
 | 
			
		||||
    $node_fr->setTitle('French Published Node');
 | 
			
		||||
    $node_fr->save();
 | 
			
		||||
    $this->assertEquals('published', $node_fr->moderation_state->value);
 | 
			
		||||
 | 
			
		||||
    // Create a pending revision of the original node.
 | 
			
		||||
    $node->moderation_state = 'draft';
 | 
			
		||||
    $node->setNewRevision(TRUE);
 | 
			
		||||
    $node->isDefaultRevision(FALSE);
 | 
			
		||||
    $node->save();
 | 
			
		||||
 | 
			
		||||
    // For the pending english revision, there should be a violation from draft
 | 
			
		||||
    // to archived.
 | 
			
		||||
    $node->moderation_state = 'archived';
 | 
			
		||||
    $violations = $node->validate();
 | 
			
		||||
    $this->assertCount(1, $violations);
 | 
			
		||||
    $this->assertEquals('Invalid state transition from Draft to Archived', $violations->get(0)->getMessage());
 | 
			
		||||
 | 
			
		||||
    // From the default french published revision, there should be none.
 | 
			
		||||
    $node_fr = Node::load($node->id())->getTranslation('fr');
 | 
			
		||||
    $this->assertEquals('published', $node_fr->moderation_state->value);
 | 
			
		||||
    $node_fr->moderation_state = 'archived';
 | 
			
		||||
    $violations = $node_fr->validate();
 | 
			
		||||
    $this->assertCount(0, $violations);
 | 
			
		||||
 | 
			
		||||
    // From the latest french revision, there should also be no violation.
 | 
			
		||||
    $node_fr = Node::load($node->id())->getTranslation('fr');
 | 
			
		||||
    $this->assertEquals('published', $node_fr->moderation_state->value);
 | 
			
		||||
    $node_fr->moderation_state = 'archived';
 | 
			
		||||
    $violations = $node_fr->validate();
 | 
			
		||||
    $this->assertCount(0, $violations);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests that content without prior moderation information can be moderated.
 | 
			
		||||
   */
 | 
			
		||||
  public function testExistingContentWithNoModeration(): void {
 | 
			
		||||
 | 
			
		||||
    $node_type = NodeType::create([
 | 
			
		||||
      'type' => 'example',
 | 
			
		||||
      'name' => 'Example',
 | 
			
		||||
    ]);
 | 
			
		||||
    $node_type->save();
 | 
			
		||||
    /** @var \Drupal\node\NodeInterface $node */
 | 
			
		||||
    $node = Node::create([
 | 
			
		||||
      'type' => 'example',
 | 
			
		||||
      'title' => 'Test title',
 | 
			
		||||
    ]);
 | 
			
		||||
    $node->save();
 | 
			
		||||
 | 
			
		||||
    $nid = $node->id();
 | 
			
		||||
 | 
			
		||||
    // Enable moderation for our node type.
 | 
			
		||||
    $workflow = $this->createEditorialWorkflow();
 | 
			
		||||
    $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example');
 | 
			
		||||
    $workflow->save();
 | 
			
		||||
 | 
			
		||||
    $this->setCurrentUser($this->createUser(['use editorial transition publish']));
 | 
			
		||||
    $node = Node::load($nid);
 | 
			
		||||
 | 
			
		||||
    // Having no previous state should not break validation.
 | 
			
		||||
    $violations = $node->validate();
 | 
			
		||||
 | 
			
		||||
    $this->assertCount(0, $violations);
 | 
			
		||||
 | 
			
		||||
    // Having no previous state should not break saving the node.
 | 
			
		||||
    $node->setTitle('New');
 | 
			
		||||
    $node->save();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests that content without prior moderation information can be translated.
 | 
			
		||||
   */
 | 
			
		||||
  public function testExistingMultilingualContentWithNoModeration(): void {
 | 
			
		||||
    $this->setCurrentUser($this->adminUser);
 | 
			
		||||
 | 
			
		||||
    // Enable French.
 | 
			
		||||
    ConfigurableLanguage::createFromLangcode('fr')->save();
 | 
			
		||||
 | 
			
		||||
    $node_type = NodeType::create([
 | 
			
		||||
      'type' => 'example',
 | 
			
		||||
      'name' => 'Example',
 | 
			
		||||
    ]);
 | 
			
		||||
    $node_type->save();
 | 
			
		||||
    /** @var \Drupal\node\NodeInterface $node */
 | 
			
		||||
    $node = Node::create([
 | 
			
		||||
      'type' => 'example',
 | 
			
		||||
      'title' => 'Test title',
 | 
			
		||||
      'langcode' => 'en',
 | 
			
		||||
    ]);
 | 
			
		||||
    $node->save();
 | 
			
		||||
 | 
			
		||||
    $nid = $node->id();
 | 
			
		||||
 | 
			
		||||
    $node = Node::load($nid);
 | 
			
		||||
 | 
			
		||||
    // Creating a translation shouldn't break, even though there's no previous
 | 
			
		||||
    // moderated revision for the new language.
 | 
			
		||||
    $node_fr = $node->addTranslation('fr');
 | 
			
		||||
    $node_fr->setTitle('Francais');
 | 
			
		||||
    $node_fr->save();
 | 
			
		||||
 | 
			
		||||
    // Enable moderation for our node type.
 | 
			
		||||
    $workflow = $this->createEditorialWorkflow();
 | 
			
		||||
    $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example');
 | 
			
		||||
    $workflow->save();
 | 
			
		||||
 | 
			
		||||
    // Reload the French version of the node.
 | 
			
		||||
    $node = Node::load($nid);
 | 
			
		||||
    $node_fr = $node->getTranslation('fr');
 | 
			
		||||
 | 
			
		||||
    /** @var \Drupal\node\NodeInterface $node_fr */
 | 
			
		||||
    $node_fr->setTitle('Nouveau');
 | 
			
		||||
    $node_fr->save();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @dataProvider transitionAccessValidationTestCases
 | 
			
		||||
   */
 | 
			
		||||
  public function testTransitionAccessValidation($permissions, $target_state, $messages): void {
 | 
			
		||||
    $node_type = NodeType::create([
 | 
			
		||||
      'type' => 'example',
 | 
			
		||||
      'name' => 'Example',
 | 
			
		||||
    ]);
 | 
			
		||||
    $node_type->save();
 | 
			
		||||
    $workflow = $this->createEditorialWorkflow();
 | 
			
		||||
    $workflow->getTypePlugin()->addState('foo', 'Foo');
 | 
			
		||||
    $workflow->getTypePlugin()->addTransition('draft_to_foo', 'Draft to foo', ['draft'], 'foo');
 | 
			
		||||
    $workflow->getTypePlugin()->addTransition('foo_to_foo', 'Foo to foo', ['foo'], 'foo');
 | 
			
		||||
    $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example');
 | 
			
		||||
    $workflow->save();
 | 
			
		||||
 | 
			
		||||
    $this->setCurrentUser($this->createUser($permissions));
 | 
			
		||||
 | 
			
		||||
    $node = Node::create([
 | 
			
		||||
      'type' => 'example',
 | 
			
		||||
      'title' => 'Test content',
 | 
			
		||||
      'moderation_state' => $target_state,
 | 
			
		||||
    ]);
 | 
			
		||||
    $this->assertTrue($node->isNew());
 | 
			
		||||
    $violations = $node->validate();
 | 
			
		||||
    $this->assertSameSize($messages, $violations);
 | 
			
		||||
    foreach ($messages as $i => $message) {
 | 
			
		||||
      $this->assertEquals($message, $violations->get($i)->getMessage());
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Test cases for ::testTransitionAccessValidation.
 | 
			
		||||
   */
 | 
			
		||||
  public static function transitionAccessValidationTestCases() {
 | 
			
		||||
    return [
 | 
			
		||||
      'Invalid transition, no permissions validated' => [
 | 
			
		||||
        [],
 | 
			
		||||
        'archived',
 | 
			
		||||
        ['Invalid state transition from Draft to Archived'],
 | 
			
		||||
      ],
 | 
			
		||||
      'Valid transition, missing permission' => [
 | 
			
		||||
        [],
 | 
			
		||||
        'published',
 | 
			
		||||
        ['You do not have access to transition from Draft to Published'],
 | 
			
		||||
      ],
 | 
			
		||||
      'Valid transition, granted published permission' => [
 | 
			
		||||
        ['use editorial transition publish'],
 | 
			
		||||
        'published',
 | 
			
		||||
        [],
 | 
			
		||||
      ],
 | 
			
		||||
      'Valid transition, granted draft permission' => [
 | 
			
		||||
        ['use editorial transition create_new_draft'],
 | 
			
		||||
        'draft',
 | 
			
		||||
        [],
 | 
			
		||||
      ],
 | 
			
		||||
      'Valid transition, incorrect permission granted' => [
 | 
			
		||||
        ['use editorial transition create_new_draft'],
 | 
			
		||||
        'published',
 | 
			
		||||
        ['You do not have access to transition from Draft to Published'],
 | 
			
		||||
      ],
 | 
			
		||||
      // Test with an additional state and set of transitions, since the
 | 
			
		||||
      // "published" transition can start from either "draft" or "published", it
 | 
			
		||||
      // does not capture bugs that fail to correctly distinguish the initial
 | 
			
		||||
      // workflow state from the set state of a new entity.
 | 
			
		||||
      'Valid transition, granted foo permission' => [
 | 
			
		||||
        ['use editorial transition draft_to_foo'],
 | 
			
		||||
        'foo',
 | 
			
		||||
        [],
 | 
			
		||||
      ],
 | 
			
		||||
      'Valid transition, incorrect  foo permission granted' => [
 | 
			
		||||
        ['use editorial transition foo_to_foo'],
 | 
			
		||||
        'foo',
 | 
			
		||||
        ['You do not have access to transition from Draft to Foo'],
 | 
			
		||||
      ],
 | 
			
		||||
    ];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,151 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\content_moderation\Kernel;
 | 
			
		||||
 | 
			
		||||
use Drupal\content_moderation\Entity\Handler\ModerationHandler;
 | 
			
		||||
use Drupal\content_moderation\EntityTypeInfo;
 | 
			
		||||
use Drupal\entity_test\Entity\EntityTestBundle;
 | 
			
		||||
use Drupal\KernelTests\KernelTestBase;
 | 
			
		||||
use Drupal\Tests\content_moderation\Traits\ContentModerationTestTrait;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @coversDefaultClass \Drupal\content_moderation\EntityTypeInfo
 | 
			
		||||
 *
 | 
			
		||||
 * @group content_moderation
 | 
			
		||||
 */
 | 
			
		||||
class EntityTypeInfoTest extends KernelTestBase {
 | 
			
		||||
 | 
			
		||||
  use ContentModerationTestTrait;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = [
 | 
			
		||||
    'content_moderation',
 | 
			
		||||
    'workflows',
 | 
			
		||||
    'entity_test',
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The entity type manager.
 | 
			
		||||
   *
 | 
			
		||||
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
 | 
			
		||||
   */
 | 
			
		||||
  protected $entityTypeManager;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The entity type info class.
 | 
			
		||||
   *
 | 
			
		||||
   * @var \Drupal\content_moderation\EntityTypeInfo
 | 
			
		||||
   */
 | 
			
		||||
  protected $entityTypeInfo;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The entity field manager.
 | 
			
		||||
   *
 | 
			
		||||
   * @var \Drupal\Core\Entity\EntityFieldManagerInterface
 | 
			
		||||
   */
 | 
			
		||||
  protected $entityFieldManager;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
 | 
			
		||||
    $this->entityTypeInfo = $this->container->get('class_resolver')->getInstanceFromDefinition(EntityTypeInfo::class);
 | 
			
		||||
    $this->entityTypeManager = $this->container->get('entity_type.manager');
 | 
			
		||||
    $this->entityFieldManager = $this->container->get('entity_field.manager');
 | 
			
		||||
 | 
			
		||||
    $this->installConfig(['content_moderation']);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @covers ::entityBaseFieldInfo
 | 
			
		||||
   */
 | 
			
		||||
  public function testEntityBaseFieldInfo(): void {
 | 
			
		||||
    $definition = $this->entityTypeManager->getDefinition('entity_test');
 | 
			
		||||
    $definition->setHandlerClass('moderation', ModerationHandler::class);
 | 
			
		||||
 | 
			
		||||
    $this->enableModeration('entity_test', 'entity_test');
 | 
			
		||||
    $base_fields = $this->entityTypeInfo->entityBaseFieldInfo($definition);
 | 
			
		||||
 | 
			
		||||
    $this->assertFalse($base_fields['moderation_state']->isReadOnly());
 | 
			
		||||
    $this->assertTrue($base_fields['moderation_state']->isComputed());
 | 
			
		||||
    $this->assertTrue($base_fields['moderation_state']->isTranslatable());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the correct entity types have moderation added.
 | 
			
		||||
   *
 | 
			
		||||
   * @covers ::entityTypeAlter
 | 
			
		||||
   *
 | 
			
		||||
   * @dataProvider providerTestEntityTypeAlter
 | 
			
		||||
   */
 | 
			
		||||
  public function testEntityTypeAlter($entity_type_id, $moderatable): void {
 | 
			
		||||
    $entity_types = $this->entityTypeManager->getDefinitions();
 | 
			
		||||
    $this->assertSame($moderatable, $entity_types[$entity_type_id]->hasHandlerClass('moderation'));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Provides test data for testEntityTypeAlter().
 | 
			
		||||
   *
 | 
			
		||||
   * @return array
 | 
			
		||||
   *   An array of test cases, where each test case is an array with the
 | 
			
		||||
   *   following values:
 | 
			
		||||
   *   - An entity type ID.
 | 
			
		||||
   *   - Whether the entity type is moderatable or not.
 | 
			
		||||
   */
 | 
			
		||||
  public static function providerTestEntityTypeAlter() {
 | 
			
		||||
    $tests = [];
 | 
			
		||||
    $tests['non_internal_non_revisionable'] = ['entity_test', FALSE];
 | 
			
		||||
    $tests['non_internal_revisionable'] = ['entity_test_rev', TRUE];
 | 
			
		||||
    $tests['internal_non_revisionable'] = ['entity_test_no_label', FALSE];
 | 
			
		||||
    $tests['internal_revisionable'] = ['content_moderation_state', FALSE];
 | 
			
		||||
    return $tests;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @covers ::entityBaseFieldInfo
 | 
			
		||||
   */
 | 
			
		||||
  public function testBaseFieldOnlyAddedToModeratedEntityTypes(): void {
 | 
			
		||||
    $definition = $this->entityTypeManager->getDefinition('entity_test_with_bundle');
 | 
			
		||||
 | 
			
		||||
    EntityTestBundle::create([
 | 
			
		||||
      'id' => 'moderated',
 | 
			
		||||
    ])->save();
 | 
			
		||||
    EntityTestBundle::create([
 | 
			
		||||
      'id' => 'unmoderated',
 | 
			
		||||
    ])->save();
 | 
			
		||||
 | 
			
		||||
    $base_fields = $this->entityTypeInfo->entityBaseFieldInfo($definition);
 | 
			
		||||
    $this->assertFalse(isset($base_fields['moderation_state']));
 | 
			
		||||
 | 
			
		||||
    $this->enableModeration('entity_test_with_bundle', 'moderated');
 | 
			
		||||
    $base_fields = $this->entityTypeInfo->entityBaseFieldInfo($definition);
 | 
			
		||||
    $this->assertTrue(isset($base_fields['moderation_state']));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests entity base field provider.
 | 
			
		||||
   */
 | 
			
		||||
  public function testEntityBaseFieldProvider(): void {
 | 
			
		||||
    $this->enableModeration('entity_test_mulrev', 'entity_test_mulrev');
 | 
			
		||||
    $this->container->get('state')->set('entity_test.field_test_item', TRUE);
 | 
			
		||||
 | 
			
		||||
    $field_definitions = $this->entityFieldManager->getFieldDefinitions('entity_test_mulrev', 'entity_test_mulrev');
 | 
			
		||||
    $this->assertEquals('entity_test', $field_definitions['field_test_item']->getProvider());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Add moderation to an entity type and bundle.
 | 
			
		||||
   */
 | 
			
		||||
  protected function enableModeration($entity_type_id, $bundle): void {
 | 
			
		||||
    $workflow = $this->createEditorialWorkflow();
 | 
			
		||||
    $workflow->getTypePlugin()->addEntityTypeAndBundle($entity_type_id, $bundle);
 | 
			
		||||
    $workflow->save();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,93 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\content_moderation\Kernel;
 | 
			
		||||
 | 
			
		||||
use Drupal\entity_test\Entity\EntityTestRev;
 | 
			
		||||
use Drupal\KernelTests\KernelTestBase;
 | 
			
		||||
use Drupal\node\Entity\Node;
 | 
			
		||||
use Drupal\node\Entity\NodeType;
 | 
			
		||||
use Drupal\Tests\content_moderation\Traits\ContentModerationTestTrait;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests the correct initial states are set on install.
 | 
			
		||||
 *
 | 
			
		||||
 * @group content_moderation
 | 
			
		||||
 */
 | 
			
		||||
class InitialStateTest extends KernelTestBase {
 | 
			
		||||
 | 
			
		||||
  use ContentModerationTestTrait;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = [
 | 
			
		||||
    'entity_test',
 | 
			
		||||
    'node',
 | 
			
		||||
    'user',
 | 
			
		||||
    'system',
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
 | 
			
		||||
    $this->installSchema('node', 'node_access');
 | 
			
		||||
    $this->installEntitySchema('node');
 | 
			
		||||
    $this->installEntitySchema('user');
 | 
			
		||||
    $this->installEntitySchema('entity_test_rev');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the correct initial state.
 | 
			
		||||
   */
 | 
			
		||||
  public function testInitialState(): void {
 | 
			
		||||
    $node_type = NodeType::create([
 | 
			
		||||
      'type' => 'example',
 | 
			
		||||
      'name' => 'Example',
 | 
			
		||||
    ]);
 | 
			
		||||
    $node_type->save();
 | 
			
		||||
 | 
			
		||||
    // Test with an entity type that implements EntityPublishedInterface.
 | 
			
		||||
    $unpublished_node = Node::create([
 | 
			
		||||
      'type' => 'example',
 | 
			
		||||
      'title' => 'Unpublished node',
 | 
			
		||||
      'status' => 0,
 | 
			
		||||
    ]);
 | 
			
		||||
    $unpublished_node->save();
 | 
			
		||||
 | 
			
		||||
    $published_node = Node::create([
 | 
			
		||||
      'type' => 'example',
 | 
			
		||||
      'title' => 'Published node',
 | 
			
		||||
      'status' => 1,
 | 
			
		||||
    ]);
 | 
			
		||||
    $published_node->save();
 | 
			
		||||
 | 
			
		||||
    // Test with an entity type that doesn't implement EntityPublishedInterface.
 | 
			
		||||
    $entity_test = EntityTestRev::create();
 | 
			
		||||
    $entity_test->save();
 | 
			
		||||
 | 
			
		||||
    \Drupal::service('module_installer')->install(['content_moderation'], TRUE);
 | 
			
		||||
    $workflow = $this->createEditorialWorkflow();
 | 
			
		||||
    $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example');
 | 
			
		||||
    $workflow->getTypePlugin()->addEntityTypeAndBundle('entity_test_rev', 'entity_test_rev');
 | 
			
		||||
    $workflow->save();
 | 
			
		||||
 | 
			
		||||
    $loaded_unpublished_node = Node::load($unpublished_node->id());
 | 
			
		||||
    $loaded_published_node = Node::load($published_node->id());
 | 
			
		||||
    $loaded_entity_test = EntityTestRev::load($entity_test->id());
 | 
			
		||||
    $this->assertEquals('draft', $loaded_unpublished_node->moderation_state->value);
 | 
			
		||||
    $this->assertEquals('published', $loaded_published_node->moderation_state->value);
 | 
			
		||||
    $this->assertEquals('draft', $loaded_entity_test->moderation_state->value);
 | 
			
		||||
 | 
			
		||||
    $presave_node = Node::create([
 | 
			
		||||
      'type' => 'example',
 | 
			
		||||
      'title' => 'Presave node',
 | 
			
		||||
    ]);
 | 
			
		||||
    $this->assertEquals('draft', $presave_node->moderation_state->value);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,213 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\content_moderation\Kernel;
 | 
			
		||||
 | 
			
		||||
use Drupal\entity_test\Entity\EntityTestMulRevPub;
 | 
			
		||||
use Drupal\entity_test\Entity\EntityTestRev;
 | 
			
		||||
use Drupal\KernelTests\KernelTestBase;
 | 
			
		||||
use Drupal\language\Entity\ConfigurableLanguage;
 | 
			
		||||
use Drupal\Tests\content_moderation\Traits\ContentModerationTestTrait;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @coversDefaultClass \Drupal\content_moderation\ModerationInformation
 | 
			
		||||
 * @group content_moderation
 | 
			
		||||
 */
 | 
			
		||||
class ModerationInformationTest extends KernelTestBase {
 | 
			
		||||
 | 
			
		||||
  use ContentModerationTestTrait;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = [
 | 
			
		||||
    'content_moderation',
 | 
			
		||||
    'entity_test',
 | 
			
		||||
    'user',
 | 
			
		||||
    'workflows',
 | 
			
		||||
    'language',
 | 
			
		||||
    'content_translation',
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The moderation information service.
 | 
			
		||||
   *
 | 
			
		||||
   * @var \Drupal\content_moderation\ModerationInformationInterface
 | 
			
		||||
   */
 | 
			
		||||
  protected $moderationInformation;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
 | 
			
		||||
    $this->installEntitySchema('entity_test_rev');
 | 
			
		||||
    $this->installEntitySchema('entity_test_mulrevpub');
 | 
			
		||||
    $this->installEntitySchema('content_moderation_state');
 | 
			
		||||
    $this->installConfig(['content_moderation']);
 | 
			
		||||
 | 
			
		||||
    $this->moderationInformation = $this->container->get('content_moderation.moderation_information');
 | 
			
		||||
 | 
			
		||||
    ConfigurableLanguage::createFromLangcode('de')->save();
 | 
			
		||||
 | 
			
		||||
    $workflow = $this->createEditorialWorkflow();
 | 
			
		||||
    $workflow->getTypePlugin()->addEntityTypeAndBundle('entity_test_mulrevpub', 'entity_test_mulrevpub');
 | 
			
		||||
    $workflow->getTypePlugin()->addEntityTypeAndBundle('entity_test_rev', 'entity_test_rev');
 | 
			
		||||
    $workflow->save();
 | 
			
		||||
 | 
			
		||||
    $this->container->get('content_translation.manager')->setEnabled('entity_test_mulrevpub', 'entity_test_mulrevpub', TRUE);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @covers ::getDefaultRevisionId
 | 
			
		||||
   */
 | 
			
		||||
  public function testGetDefaultRevisionId(): void {
 | 
			
		||||
    $entity_test_rev = EntityTestRev::create([
 | 
			
		||||
      'name' => 'Default Revision',
 | 
			
		||||
      'moderation_state' => 'published',
 | 
			
		||||
    ]);
 | 
			
		||||
    $entity_test_rev->save();
 | 
			
		||||
 | 
			
		||||
    $entity_test_rev->name = 'Pending revision';
 | 
			
		||||
    $entity_test_rev->moderation_state = 'draft';
 | 
			
		||||
    $entity_test_rev->save();
 | 
			
		||||
 | 
			
		||||
    // Check that moderation information service returns the correct default
 | 
			
		||||
    // revision ID.
 | 
			
		||||
    $default_revision_id = $this->moderationInformation->getDefaultRevisionId('entity_test_rev', $entity_test_rev->id());
 | 
			
		||||
    $this->assertSame(1, $default_revision_id);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @covers ::isDefaultRevisionPublished
 | 
			
		||||
   * @dataProvider isDefaultRevisionPublishedTestCases
 | 
			
		||||
   */
 | 
			
		||||
  public function testIsDefaultRevisionPublished($initial_state, $final_state, $initial_is_default_published, $final_is_default_published): void {
 | 
			
		||||
    $entity = EntityTestMulRevPub::create([
 | 
			
		||||
      'moderation_state' => $initial_state,
 | 
			
		||||
    ]);
 | 
			
		||||
    $entity->save();
 | 
			
		||||
    $this->assertEquals($initial_is_default_published, $this->moderationInformation->isDefaultRevisionPublished($entity));
 | 
			
		||||
 | 
			
		||||
    $entity->moderation_state = $final_state;
 | 
			
		||||
    $entity->save();
 | 
			
		||||
    $this->assertEquals($final_is_default_published, $this->moderationInformation->isDefaultRevisionPublished($entity));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Test cases for ::testIsDefaultRevisionPublished.
 | 
			
		||||
   */
 | 
			
		||||
  public static function isDefaultRevisionPublishedTestCases() {
 | 
			
		||||
    return [
 | 
			
		||||
      'Draft to draft' => [
 | 
			
		||||
        'draft',
 | 
			
		||||
        'draft',
 | 
			
		||||
        FALSE,
 | 
			
		||||
        FALSE,
 | 
			
		||||
      ],
 | 
			
		||||
      'Draft to published' => [
 | 
			
		||||
        'draft',
 | 
			
		||||
        'published',
 | 
			
		||||
        FALSE,
 | 
			
		||||
        TRUE,
 | 
			
		||||
      ],
 | 
			
		||||
      'Published to published' => [
 | 
			
		||||
        'published',
 | 
			
		||||
        'published',
 | 
			
		||||
        TRUE,
 | 
			
		||||
        TRUE,
 | 
			
		||||
      ],
 | 
			
		||||
      'Published to draft' => [
 | 
			
		||||
        'published',
 | 
			
		||||
        'draft',
 | 
			
		||||
        TRUE,
 | 
			
		||||
        TRUE,
 | 
			
		||||
      ],
 | 
			
		||||
    ];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @covers ::isDefaultRevisionPublished
 | 
			
		||||
   */
 | 
			
		||||
  public function testIsDefaultRevisionPublishedMultilingual(): void {
 | 
			
		||||
    $entity = EntityTestMulRevPub::create([
 | 
			
		||||
      'moderation_state' => 'draft',
 | 
			
		||||
    ]);
 | 
			
		||||
    $entity->save();
 | 
			
		||||
    $this->assertEquals('draft', $entity->moderation_state->value);
 | 
			
		||||
 | 
			
		||||
    $translated = $entity->addTranslation('de');
 | 
			
		||||
    $translated->moderation_state = 'published';
 | 
			
		||||
    $translated->save();
 | 
			
		||||
    $this->assertEquals('published', $translated->moderation_state->value);
 | 
			
		||||
 | 
			
		||||
    // Test a scenario where the default revision exists with the default
 | 
			
		||||
    // language in a draft state and a non-default language in a published
 | 
			
		||||
    // state. The method returns TRUE if any of the languages for the default
 | 
			
		||||
    // revision are in a published state.
 | 
			
		||||
    $this->assertTrue($this->moderationInformation->isDefaultRevisionPublished($entity));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @covers ::hasPendingRevision
 | 
			
		||||
   */
 | 
			
		||||
  public function testHasPendingRevision(): void {
 | 
			
		||||
    $entity = EntityTestMulRevPub::create([
 | 
			
		||||
      'moderation_state' => 'published',
 | 
			
		||||
    ]);
 | 
			
		||||
    $entity->save();
 | 
			
		||||
 | 
			
		||||
    // Add a translation as a new revision.
 | 
			
		||||
    $translated = $entity->addTranslation('de');
 | 
			
		||||
    $translated->moderation_state = 'published';
 | 
			
		||||
    $translated->setNewRevision(TRUE);
 | 
			
		||||
 | 
			
		||||
    // Test a scenario where the default revision exists with the default
 | 
			
		||||
    // language in a published state and a non-default language in an unsaved
 | 
			
		||||
    // state.
 | 
			
		||||
    $this->assertFalse($this->moderationInformation->hasPendingRevision($translated));
 | 
			
		||||
 | 
			
		||||
    // Save the translation and assert there is no pending revision.
 | 
			
		||||
    $translated->save();
 | 
			
		||||
    $this->assertFalse($this->moderationInformation->hasPendingRevision($translated));
 | 
			
		||||
 | 
			
		||||
    // Create a new draft for the translation and assert there is a pending
 | 
			
		||||
    // revision.
 | 
			
		||||
    $translated->moderation_state = 'draft';
 | 
			
		||||
    $translated->setNewRevision(TRUE);
 | 
			
		||||
    $translated->save();
 | 
			
		||||
    $this->assertTrue($this->moderationInformation->hasPendingRevision($translated));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @covers ::getOriginalState
 | 
			
		||||
   */
 | 
			
		||||
  public function testGetOriginalState(): void {
 | 
			
		||||
    $entity = EntityTestMulRevPub::create([
 | 
			
		||||
      'moderation_state' => 'published',
 | 
			
		||||
    ]);
 | 
			
		||||
    $entity->save();
 | 
			
		||||
    $entity->moderation_state = 'foo';
 | 
			
		||||
    $this->assertEquals('published', $this->moderationInformation->getOriginalState($entity)->id());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @covers ::getOriginalState
 | 
			
		||||
   */
 | 
			
		||||
  public function testGetOriginalStateMultilingual(): void {
 | 
			
		||||
    $entity = EntityTestMulRevPub::create([
 | 
			
		||||
      'moderation_state' => 'draft',
 | 
			
		||||
    ]);
 | 
			
		||||
    $entity->save();
 | 
			
		||||
 | 
			
		||||
    $translated = $entity->addTranslation('de', $entity->toArray());
 | 
			
		||||
    $translated->moderation_state = 'published';
 | 
			
		||||
    $translated->save();
 | 
			
		||||
 | 
			
		||||
    $translated->moderation_state = 'foo';
 | 
			
		||||
    $this->assertEquals('published', $this->moderationInformation->getOriginalState($translated)->id());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,459 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\content_moderation\Kernel;
 | 
			
		||||
 | 
			
		||||
use Drupal\KernelTests\KernelTestBase;
 | 
			
		||||
use Drupal\language\Entity\ConfigurableLanguage;
 | 
			
		||||
use Drupal\node\Entity\Node;
 | 
			
		||||
use Drupal\node\Entity\NodeType;
 | 
			
		||||
use Drupal\Tests\content_moderation\Traits\ContentModerationTestTrait;
 | 
			
		||||
use Drupal\Tests\user\Traits\UserCreationTrait;
 | 
			
		||||
use Drupal\workflows\Entity\Workflow;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @coversDefaultClass \Drupal\content_moderation\Plugin\Field\ModerationStateFieldItemList
 | 
			
		||||
 *
 | 
			
		||||
 * @group content_moderation
 | 
			
		||||
 */
 | 
			
		||||
class ModerationStateFieldItemListTest extends KernelTestBase {
 | 
			
		||||
 | 
			
		||||
  use ContentModerationTestTrait;
 | 
			
		||||
  use UserCreationTrait;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = [
 | 
			
		||||
    'node',
 | 
			
		||||
    'content_moderation',
 | 
			
		||||
    'user',
 | 
			
		||||
    'system',
 | 
			
		||||
    'language',
 | 
			
		||||
    'workflows',
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @var \Drupal\node\NodeInterface
 | 
			
		||||
   */
 | 
			
		||||
  protected $testNode;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
 | 
			
		||||
    $this->installSchema('node', 'node_access');
 | 
			
		||||
    $this->installEntitySchema('node');
 | 
			
		||||
    $this->installEntitySchema('user');
 | 
			
		||||
    $this->installEntitySchema('content_moderation_state');
 | 
			
		||||
    $this->installConfig('content_moderation');
 | 
			
		||||
 | 
			
		||||
    NodeType::create([
 | 
			
		||||
      'type' => 'unmoderated',
 | 
			
		||||
      'name' => 'Unmoderated',
 | 
			
		||||
    ])->save();
 | 
			
		||||
 | 
			
		||||
    $node_type = NodeType::create([
 | 
			
		||||
      'type' => 'example',
 | 
			
		||||
      'name' => 'Example',
 | 
			
		||||
    ]);
 | 
			
		||||
    $node_type->save();
 | 
			
		||||
    $workflow = $this->createEditorialWorkflow();
 | 
			
		||||
    $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example');
 | 
			
		||||
    $workflow->save();
 | 
			
		||||
 | 
			
		||||
    $this->testNode = Node::create([
 | 
			
		||||
      'type' => 'example',
 | 
			
		||||
      'title' => 'Test title',
 | 
			
		||||
    ]);
 | 
			
		||||
    $this->testNode->save();
 | 
			
		||||
    \Drupal::entityTypeManager()->getStorage('node')->resetCache();
 | 
			
		||||
    $this->testNode = Node::load($this->testNode->id());
 | 
			
		||||
 | 
			
		||||
    ConfigurableLanguage::createFromLangcode('de')->save();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the field item list when accessing an index.
 | 
			
		||||
   */
 | 
			
		||||
  public function testArrayIndex(): void {
 | 
			
		||||
    $this->assertFalse($this->testNode->isPublished());
 | 
			
		||||
    $this->assertEquals('draft', $this->testNode->moderation_state[0]->value);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the field item list when iterating.
 | 
			
		||||
   */
 | 
			
		||||
  public function testArrayIteration(): void {
 | 
			
		||||
    $states = [];
 | 
			
		||||
    foreach ($this->testNode->moderation_state as $item) {
 | 
			
		||||
      $states[] = $item->value;
 | 
			
		||||
    }
 | 
			
		||||
    $this->assertEquals(['draft'], $states);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @covers ::getValue
 | 
			
		||||
   */
 | 
			
		||||
  public function testGetValue(): void {
 | 
			
		||||
    $this->assertEquals([['value' => 'draft']], $this->testNode->moderation_state->getValue());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @covers ::get
 | 
			
		||||
   */
 | 
			
		||||
  public function testGet(): void {
 | 
			
		||||
    $this->assertEquals('draft', $this->testNode->moderation_state->get(0)->value);
 | 
			
		||||
    $this->expectException(\InvalidArgumentException::class);
 | 
			
		||||
    $this->testNode->moderation_state->get(2);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the item list when it is emptied and appended to.
 | 
			
		||||
   */
 | 
			
		||||
  public function testEmptyStateAndAppend(): void {
 | 
			
		||||
    // This test case mimics the lifecycle of an entity that is being patched in
 | 
			
		||||
    // a rest resource.
 | 
			
		||||
    $this->testNode->moderation_state->setValue([]);
 | 
			
		||||
    $this->assertTrue($this->testNode->moderation_state->isEmpty());
 | 
			
		||||
    $this->assertEmptiedModerationFieldItemList();
 | 
			
		||||
 | 
			
		||||
    $this->testNode->moderation_state->appendItem();
 | 
			
		||||
    $this->assertEquals(1, $this->testNode->moderation_state->count());
 | 
			
		||||
    $this->assertEquals(NULL, $this->testNode->moderation_state->value);
 | 
			
		||||
    $this->assertEmptiedModerationFieldItemList();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests an empty value assigned to the field item.
 | 
			
		||||
   */
 | 
			
		||||
  public function testEmptyFieldItem(): void {
 | 
			
		||||
    $this->testNode->moderation_state->value = '';
 | 
			
		||||
    $this->assertEquals('', $this->testNode->moderation_state->value);
 | 
			
		||||
    $this->assertEmptiedModerationFieldItemList();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests an empty value assigned to the field item list.
 | 
			
		||||
   */
 | 
			
		||||
  public function testEmptyFieldItemList(): void {
 | 
			
		||||
    $this->testNode->moderation_state = '';
 | 
			
		||||
    $this->assertEquals('', $this->testNode->moderation_state->value);
 | 
			
		||||
    $this->assertEmptiedModerationFieldItemList();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the field item when it is unset.
 | 
			
		||||
   */
 | 
			
		||||
  public function testUnsetItemList(): void {
 | 
			
		||||
    unset($this->testNode->moderation_state);
 | 
			
		||||
    $this->assertEquals(NULL, $this->testNode->moderation_state->value);
 | 
			
		||||
    $this->assertEmptiedModerationFieldItemList();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the field item when it is assigned NULL.
 | 
			
		||||
   */
 | 
			
		||||
  public function testAssignNullItemList(): void {
 | 
			
		||||
    $this->testNode->moderation_state = NULL;
 | 
			
		||||
    $this->assertEquals(NULL, $this->testNode->moderation_state->value);
 | 
			
		||||
    $this->assertEmptiedModerationFieldItemList();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Assert the set of expectations when the moderation state field is emptied.
 | 
			
		||||
   *
 | 
			
		||||
   * @internal
 | 
			
		||||
   */
 | 
			
		||||
  protected function assertEmptiedModerationFieldItemList(): void {
 | 
			
		||||
    $this->assertTrue($this->testNode->moderation_state->isEmpty());
 | 
			
		||||
    // Test the empty value causes a violation in the entity.
 | 
			
		||||
    $violations = $this->testNode->validate();
 | 
			
		||||
    $this->assertCount(1, $violations);
 | 
			
		||||
    $this->assertEquals('This value should not be null.', $violations->get(0)->getMessage());
 | 
			
		||||
    // Test that incorrectly saving the entity regardless will not produce a
 | 
			
		||||
    // change in the moderation state.
 | 
			
		||||
    $this->testNode->save();
 | 
			
		||||
    $this->assertEquals('draft', Node::load($this->testNode->id())->moderation_state->value);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the list class with a non moderated entity.
 | 
			
		||||
   */
 | 
			
		||||
  public function testNonModeratedEntity(): void {
 | 
			
		||||
    $unmoderated_node = Node::create([
 | 
			
		||||
      'type' => 'unmoderated',
 | 
			
		||||
      'title' => 'Test title',
 | 
			
		||||
    ]);
 | 
			
		||||
    $unmoderated_node->save();
 | 
			
		||||
    $this->assertEquals(0, $unmoderated_node->moderation_state->count());
 | 
			
		||||
 | 
			
		||||
    $unmoderated_node->moderation_state = NULL;
 | 
			
		||||
    $this->assertEquals(0, $unmoderated_node->moderation_state->count());
 | 
			
		||||
    $this->assertCount(0, $unmoderated_node->validate());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests that moderation state changes also change the related entity state.
 | 
			
		||||
   *
 | 
			
		||||
   * @dataProvider moderationStateChangesTestCases
 | 
			
		||||
   */
 | 
			
		||||
  public function testModerationStateChanges($initial_state, $final_state, $first_published, $first_is_default, $second_published, $second_is_default): void {
 | 
			
		||||
    $this->testNode->moderation_state->value = $initial_state;
 | 
			
		||||
    $this->assertEquals($first_published, $this->testNode->isPublished());
 | 
			
		||||
    $this->assertEquals($first_is_default, $this->testNode->isDefaultRevision());
 | 
			
		||||
    $this->testNode->save();
 | 
			
		||||
 | 
			
		||||
    $this->testNode->moderation_state->value = $final_state;
 | 
			
		||||
    $this->assertEquals($second_published, $this->testNode->isPublished());
 | 
			
		||||
    $this->assertEquals($second_is_default, $this->testNode->isDefaultRevision());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Data provider for ::testModerationStateChanges.
 | 
			
		||||
   */
 | 
			
		||||
  public static function moderationStateChangesTestCases() {
 | 
			
		||||
    return [
 | 
			
		||||
      'Draft to draft' => [
 | 
			
		||||
        'draft',
 | 
			
		||||
        'draft',
 | 
			
		||||
        FALSE,
 | 
			
		||||
        TRUE,
 | 
			
		||||
        FALSE,
 | 
			
		||||
        TRUE,
 | 
			
		||||
      ],
 | 
			
		||||
      'Draft to published' => [
 | 
			
		||||
        'draft',
 | 
			
		||||
        'published',
 | 
			
		||||
        FALSE,
 | 
			
		||||
        TRUE,
 | 
			
		||||
        TRUE,
 | 
			
		||||
        TRUE,
 | 
			
		||||
      ],
 | 
			
		||||
      'Published to published' => [
 | 
			
		||||
        'published',
 | 
			
		||||
        'published',
 | 
			
		||||
        TRUE,
 | 
			
		||||
        TRUE,
 | 
			
		||||
        TRUE,
 | 
			
		||||
        TRUE,
 | 
			
		||||
      ],
 | 
			
		||||
      'Published to draft' => [
 | 
			
		||||
        'published',
 | 
			
		||||
        'draft',
 | 
			
		||||
        TRUE,
 | 
			
		||||
        TRUE,
 | 
			
		||||
        FALSE,
 | 
			
		||||
        FALSE,
 | 
			
		||||
      ],
 | 
			
		||||
    ];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests updating the state for an entity without a workflow.
 | 
			
		||||
   */
 | 
			
		||||
  public function testEntityWithNoWorkflow(): void {
 | 
			
		||||
    $node_type = NodeType::create([
 | 
			
		||||
      'type' => 'example_no_workflow',
 | 
			
		||||
      'name' => 'No-Workflow example',
 | 
			
		||||
    ]);
 | 
			
		||||
    $node_type->save();
 | 
			
		||||
    $test_node = Node::create([
 | 
			
		||||
      'type' => 'example_no_workflow',
 | 
			
		||||
      'title' => 'Test node with no workflow',
 | 
			
		||||
    ]);
 | 
			
		||||
    $test_node->save();
 | 
			
		||||
 | 
			
		||||
    /** @var \Drupal\content_moderation\ModerationInformationInterface $content_moderation_info */
 | 
			
		||||
    $content_moderation_info = \Drupal::service('content_moderation.moderation_information');
 | 
			
		||||
    $workflow = $content_moderation_info->getWorkflowForEntity($test_node);
 | 
			
		||||
    $this->assertNull($workflow);
 | 
			
		||||
 | 
			
		||||
    $this->assertTrue($test_node->isPublished());
 | 
			
		||||
    $test_node->moderation_state->setValue('draft');
 | 
			
		||||
    // The entity is still published because there is not a workflow.
 | 
			
		||||
    $this->assertTrue($test_node->isPublished());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the moderation_state field after an entity has been serialized.
 | 
			
		||||
   *
 | 
			
		||||
   * @dataProvider entityUnserializeTestCases
 | 
			
		||||
   */
 | 
			
		||||
  public function testEntityUnserialize($state, $default, $published): void {
 | 
			
		||||
    $this->testNode->moderation_state->value = $state;
 | 
			
		||||
 | 
			
		||||
    $this->assertEquals($state, $this->testNode->moderation_state->value);
 | 
			
		||||
    $this->assertEquals($default, $this->testNode->isDefaultRevision());
 | 
			
		||||
    $this->assertEquals($published, $this->testNode->isPublished());
 | 
			
		||||
 | 
			
		||||
    $unserialized = unserialize(serialize($this->testNode));
 | 
			
		||||
 | 
			
		||||
    $this->assertEquals($state, $unserialized->moderation_state->value);
 | 
			
		||||
    $this->assertEquals($default, $unserialized->isDefaultRevision());
 | 
			
		||||
    $this->assertEquals($published, $unserialized->isPublished());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Test cases for ::testEntityUnserialize.
 | 
			
		||||
   */
 | 
			
		||||
  public static function entityUnserializeTestCases() {
 | 
			
		||||
    return [
 | 
			
		||||
      'Default draft state' => [
 | 
			
		||||
        'draft',
 | 
			
		||||
        TRUE,
 | 
			
		||||
        FALSE,
 | 
			
		||||
      ],
 | 
			
		||||
      'Non-default published state' => [
 | 
			
		||||
        'published',
 | 
			
		||||
        TRUE,
 | 
			
		||||
        TRUE,
 | 
			
		||||
      ],
 | 
			
		||||
    ];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests saving a moderated node with an existing ID.
 | 
			
		||||
   *
 | 
			
		||||
   * @dataProvider moderatedEntityWithExistingIdTestCases
 | 
			
		||||
   */
 | 
			
		||||
  public function testModeratedEntityWithExistingId($state): void {
 | 
			
		||||
    $node = Node::create([
 | 
			
		||||
      'title' => 'Test title',
 | 
			
		||||
      'type' => 'example',
 | 
			
		||||
      'nid' => 999,
 | 
			
		||||
      'moderation_state' => $state,
 | 
			
		||||
    ]);
 | 
			
		||||
    $node->save();
 | 
			
		||||
    $this->assertEquals($state, $node->moderation_state->value);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests cases for ::testModeratedEntityWithExistingId.
 | 
			
		||||
   */
 | 
			
		||||
  public static function moderatedEntityWithExistingIdTestCases() {
 | 
			
		||||
    return [
 | 
			
		||||
      'Draft non-default state' => [
 | 
			
		||||
        'draft',
 | 
			
		||||
      ],
 | 
			
		||||
      'Published default state' => [
 | 
			
		||||
        'published',
 | 
			
		||||
      ],
 | 
			
		||||
    ];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Test customizing the default moderation state.
 | 
			
		||||
   */
 | 
			
		||||
  public function testWorkflowCustomizedInitialState(): void {
 | 
			
		||||
    $workflow = Workflow::load('editorial');
 | 
			
		||||
    $configuration = $workflow->getTypePlugin()->getConfiguration();
 | 
			
		||||
 | 
			
		||||
    // Test a node for a workflow that hasn't been updated to include the
 | 
			
		||||
    // 'default_moderation_state' setting. We must be backwards compatible with
 | 
			
		||||
    // configuration that was exported before this change was introduced.
 | 
			
		||||
    $this->assertFalse(isset($configuration['default_moderation_state']));
 | 
			
		||||
    $legacy_configuration_node = Node::create([
 | 
			
		||||
      'title' => 'Test title',
 | 
			
		||||
      'type' => 'example',
 | 
			
		||||
    ]);
 | 
			
		||||
    $this->assertEquals('draft', $legacy_configuration_node->moderation_state->value);
 | 
			
		||||
    $legacy_configuration_node->save();
 | 
			
		||||
    $this->assertEquals('draft', $legacy_configuration_node->moderation_state->value);
 | 
			
		||||
 | 
			
		||||
    $configuration['default_moderation_state'] = 'published';
 | 
			
		||||
    $workflow->getTypePlugin()->setConfiguration($configuration);
 | 
			
		||||
    $workflow->save();
 | 
			
		||||
 | 
			
		||||
    $updated_default_node = Node::create([
 | 
			
		||||
      'title' => 'Test title',
 | 
			
		||||
      'type' => 'example',
 | 
			
		||||
    ]);
 | 
			
		||||
    $this->assertEquals('published', $updated_default_node->moderation_state->value);
 | 
			
		||||
    $legacy_configuration_node->save();
 | 
			
		||||
    $this->assertEquals('published', $updated_default_node->moderation_state->value);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the field item list when used with existing unmoderated content.
 | 
			
		||||
   */
 | 
			
		||||
  public function testWithExistingUnmoderatedContent(): void {
 | 
			
		||||
    $node = Node::create([
 | 
			
		||||
      'title' => 'Test title',
 | 
			
		||||
      'type' => 'unmoderated',
 | 
			
		||||
    ]);
 | 
			
		||||
    $node->save();
 | 
			
		||||
    $translation = $node->addTranslation('de', $node->toArray());
 | 
			
		||||
    $translation->title = 'Translated';
 | 
			
		||||
    $translation->save();
 | 
			
		||||
 | 
			
		||||
    $workflow = Workflow::load('editorial');
 | 
			
		||||
    $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'unmoderated');
 | 
			
		||||
    $workflow->save();
 | 
			
		||||
 | 
			
		||||
    // After enabling moderation, both the original node and translation should
 | 
			
		||||
    // have a published moderation state.
 | 
			
		||||
    $node = Node::load($node->id());
 | 
			
		||||
    $translation = $node->getTranslation('de');
 | 
			
		||||
    $this->assertEquals('published', $node->moderation_state->value);
 | 
			
		||||
    $this->assertEquals('published', $translation->moderation_state->value);
 | 
			
		||||
 | 
			
		||||
    // After the node has been updated, both the original node and translation
 | 
			
		||||
    // should still have a value.
 | 
			
		||||
    $node->title = 'Updated title';
 | 
			
		||||
    $node->save();
 | 
			
		||||
    $translation = $node->getTranslation('de');
 | 
			
		||||
    $this->assertEquals('published', $node->moderation_state->value);
 | 
			
		||||
    $this->assertEquals('published', $translation->moderation_state->value);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Test generating sample values for entities with a moderation state.
 | 
			
		||||
   */
 | 
			
		||||
  public function testModerationStateSampleValues(): void {
 | 
			
		||||
    $this->container->get('current_user')->setAccount(
 | 
			
		||||
      $this->createUser([
 | 
			
		||||
        'use editorial transition create_new_draft',
 | 
			
		||||
        'use editorial transition publish',
 | 
			
		||||
      ])
 | 
			
		||||
    );
 | 
			
		||||
    $sample = $this->container->get('entity_type.manager')
 | 
			
		||||
      ->getStorage('node')
 | 
			
		||||
      ->createWithSampleValues('example');
 | 
			
		||||
    $this->assertCount(0, $sample->validate());
 | 
			
		||||
    $this->assertEquals('draft', $sample->moderation_state->value);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests field item list translation support with unmoderated content.
 | 
			
		||||
   */
 | 
			
		||||
  public function testTranslationWithExistingUnmoderatedContent(): void {
 | 
			
		||||
    $node = Node::create([
 | 
			
		||||
      'title' => 'Published en',
 | 
			
		||||
      'langcode' => 'en',
 | 
			
		||||
      'type' => 'unmoderated',
 | 
			
		||||
    ]);
 | 
			
		||||
    $node->setPublished();
 | 
			
		||||
    $node->save();
 | 
			
		||||
 | 
			
		||||
    $workflow = Workflow::load('editorial');
 | 
			
		||||
    $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'unmoderated');
 | 
			
		||||
    $workflow->save();
 | 
			
		||||
 | 
			
		||||
    $translation = $node->addTranslation('de');
 | 
			
		||||
    $translation->moderation_state = 'draft';
 | 
			
		||||
    $translation->save();
 | 
			
		||||
 | 
			
		||||
    $node_storage = $this->container->get('entity_type.manager')->getStorage('node');
 | 
			
		||||
    $node = $node_storage->loadRevision($node_storage->getLatestRevisionId($node->id()));
 | 
			
		||||
 | 
			
		||||
    $this->assertEquals('published', $node->moderation_state->value);
 | 
			
		||||
    $this->assertEquals('draft', $translation->moderation_state->value);
 | 
			
		||||
    $this->assertTrue($node->isPublished());
 | 
			
		||||
    $this->assertFalse($translation->isPublished());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,96 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\content_moderation\Kernel;
 | 
			
		||||
 | 
			
		||||
use Drupal\content_moderation\Plugin\Field\FieldWidget\ModerationStateWidget;
 | 
			
		||||
use Drupal\Core\Entity\Entity\EntityFormDisplay;
 | 
			
		||||
use Drupal\Core\Form\FormState;
 | 
			
		||||
use Drupal\KernelTests\KernelTestBase;
 | 
			
		||||
use Drupal\node\Entity\Node;
 | 
			
		||||
use Drupal\node\Entity\NodeType;
 | 
			
		||||
use Drupal\Tests\content_moderation\Traits\ContentModerationTestTrait;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @coversDefaultClass \Drupal\content_moderation\Plugin\Field\FieldWidget\ModerationStateWidget
 | 
			
		||||
 * @group content_moderation
 | 
			
		||||
 */
 | 
			
		||||
class ModerationStateWidgetTest extends KernelTestBase {
 | 
			
		||||
 | 
			
		||||
  use ContentModerationTestTrait;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = [
 | 
			
		||||
    'system',
 | 
			
		||||
    'user',
 | 
			
		||||
    'workflows',
 | 
			
		||||
    'content_moderation',
 | 
			
		||||
    'node',
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
 | 
			
		||||
    $this->installEntitySchema('content_moderation_state');
 | 
			
		||||
    $this->installEntitySchema('user');
 | 
			
		||||
    $this->installConfig(['content_moderation', 'system']);
 | 
			
		||||
 | 
			
		||||
    NodeType::create([
 | 
			
		||||
      'type' => 'moderated',
 | 
			
		||||
      'name' => 'Moderated',
 | 
			
		||||
    ])->save();
 | 
			
		||||
    NodeType::create([
 | 
			
		||||
      'type' => 'unmoderated',
 | 
			
		||||
      'name' => 'Unmoderated',
 | 
			
		||||
    ])->save();
 | 
			
		||||
 | 
			
		||||
    $workflow = $this->createEditorialWorkflow();
 | 
			
		||||
    $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'moderated');
 | 
			
		||||
    $workflow->save();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the widget does not impact a non-moderated entity.
 | 
			
		||||
   */
 | 
			
		||||
  public function testWidgetNonModeratedEntity(): void {
 | 
			
		||||
    // Create an unmoderated entity and build a form display which will include
 | 
			
		||||
    // the ModerationStateWidget plugin, in a hidden state.
 | 
			
		||||
    $entity = Node::create([
 | 
			
		||||
      'type' => 'unmoderated',
 | 
			
		||||
    ]);
 | 
			
		||||
    $entity_form_display = EntityFormDisplay::create([
 | 
			
		||||
      'targetEntityType' => 'node',
 | 
			
		||||
      'bundle' => 'unmoderated',
 | 
			
		||||
      'mode' => 'default',
 | 
			
		||||
      'status' => TRUE,
 | 
			
		||||
    ]);
 | 
			
		||||
    $form = [];
 | 
			
		||||
    $form_state = new FormState();
 | 
			
		||||
    $entity_form_display->buildForm($entity, $form, $form_state);
 | 
			
		||||
 | 
			
		||||
    // The moderation_state field should have no values for an entity that isn't
 | 
			
		||||
    // being moderated.
 | 
			
		||||
    $entity_form_display->extractFormValues($entity, $form, $form_state);
 | 
			
		||||
    $this->assertEquals(0, $entity->moderation_state->count());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @covers ::isApplicable
 | 
			
		||||
   */
 | 
			
		||||
  public function testIsApplicable(): void {
 | 
			
		||||
    // The moderation_state field definition should be applicable to our widget.
 | 
			
		||||
    $fields = $this->container->get('entity_field.manager')->getFieldDefinitions('node', 'test_type');
 | 
			
		||||
    $this->assertTrue(ModerationStateWidget::isApplicable($fields['moderation_state']));
 | 
			
		||||
    $this->assertFalse(ModerationStateWidget::isApplicable($fields['status']));
 | 
			
		||||
    // A config override should still be applicable.
 | 
			
		||||
    $field_config = $fields['moderation_state']->getConfig('moderated');
 | 
			
		||||
    $this->assertTrue(ModerationStateWidget::isApplicable($field_config));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,87 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\content_moderation\Kernel;
 | 
			
		||||
 | 
			
		||||
use Drupal\KernelTests\KernelTestBase;
 | 
			
		||||
use Drupal\node\Entity\NodeType;
 | 
			
		||||
use Drupal\Tests\content_moderation\Traits\ContentModerationTestTrait;
 | 
			
		||||
use Drupal\Tests\node\Traits\NodeCreationTrait;
 | 
			
		||||
use Drupal\Tests\user\Traits\UserCreationTrait;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests with node access enabled.
 | 
			
		||||
 *
 | 
			
		||||
 * @group content_moderation
 | 
			
		||||
 */
 | 
			
		||||
class NodeAccessTest extends KernelTestBase {
 | 
			
		||||
 | 
			
		||||
  use NodeCreationTrait;
 | 
			
		||||
  use UserCreationTrait;
 | 
			
		||||
  use ContentModerationTestTrait;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The moderation information service.
 | 
			
		||||
   *
 | 
			
		||||
   * @var \Drupal\content_moderation\ModerationInformationInterface
 | 
			
		||||
   */
 | 
			
		||||
  protected $moderationInformation;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = [
 | 
			
		||||
    'content_moderation',
 | 
			
		||||
    'filter',
 | 
			
		||||
    'node',
 | 
			
		||||
    'node_access_test',
 | 
			
		||||
    'system',
 | 
			
		||||
    'user',
 | 
			
		||||
    'workflows',
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
 | 
			
		||||
    $this->installEntitySchema('content_moderation_state');
 | 
			
		||||
    $this->installEntitySchema('node');
 | 
			
		||||
    $this->installEntitySchema('user');
 | 
			
		||||
    $this->installConfig(['content_moderation', 'filter']);
 | 
			
		||||
    $this->installSchema('node', ['node_access']);
 | 
			
		||||
 | 
			
		||||
    // Add a moderated node type.
 | 
			
		||||
    $node_type = NodeType::create([
 | 
			
		||||
      'type' => 'page',
 | 
			
		||||
      'name' => 'Page',
 | 
			
		||||
    ]);
 | 
			
		||||
    $node_type->save();
 | 
			
		||||
    $workflow = $this->createEditorialWorkflow();
 | 
			
		||||
    $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'page');
 | 
			
		||||
    $workflow->save();
 | 
			
		||||
 | 
			
		||||
    $this->moderationInformation = \Drupal::service('content_moderation.moderation_information');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @covers \Drupal\content_moderation\ModerationInformation::getDefaultRevisionId
 | 
			
		||||
   */
 | 
			
		||||
  public function testGetDefaultRevisionId(): void {
 | 
			
		||||
    // Create an admin user.
 | 
			
		||||
    $user = $this->createUser([], NULL, TRUE);
 | 
			
		||||
    \Drupal::currentUser()->setAccount($user);
 | 
			
		||||
 | 
			
		||||
    // Create a node.
 | 
			
		||||
    $node = $this->createNode(['type' => 'page']);
 | 
			
		||||
    $this->assertEquals($node->getRevisionId(), $this->moderationInformation->getDefaultRevisionId('node', $node->id()));
 | 
			
		||||
 | 
			
		||||
    // Create a non-admin user.
 | 
			
		||||
    $user = $this->createUser();
 | 
			
		||||
    \Drupal::currentUser()->setAccount($user);
 | 
			
		||||
    $this->assertEquals($node->getRevisionId(), $this->moderationInformation->getDefaultRevisionId('node', $node->id()));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,92 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\content_moderation\Kernel;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Render\RenderContext;
 | 
			
		||||
use Drupal\entity_test\Entity\EntityTestRev;
 | 
			
		||||
use Drupal\KernelTests\KernelTestBase;
 | 
			
		||||
use Drupal\Tests\content_moderation\Traits\ContentModerationTestTrait;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Test the state field formatter.
 | 
			
		||||
 *
 | 
			
		||||
 * @group content_moderation
 | 
			
		||||
 */
 | 
			
		||||
class StateFormatterTest extends KernelTestBase {
 | 
			
		||||
 | 
			
		||||
  use ContentModerationTestTrait;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = [
 | 
			
		||||
    'workflows',
 | 
			
		||||
    'content_moderation',
 | 
			
		||||
    'entity_test',
 | 
			
		||||
    'user',
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
 | 
			
		||||
    $this->installEntitySchema('entity_test_rev');
 | 
			
		||||
    $this->installEntitySchema('content_moderation_state');
 | 
			
		||||
    $this->installConfig('content_moderation');
 | 
			
		||||
 | 
			
		||||
    $workflow = $this->createEditorialWorkflow();
 | 
			
		||||
    $workflow->getTypePlugin()->addEntityTypeAndBundle('entity_test_rev', 'entity_test_rev');
 | 
			
		||||
    $workflow->save();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the embed field.
 | 
			
		||||
   *
 | 
			
		||||
   * @dataProvider formatterTestCases
 | 
			
		||||
   */
 | 
			
		||||
  public function testStateFieldFormatter($field_value, $formatter_settings, $expected_output): void {
 | 
			
		||||
    $entity = EntityTestRev::create([
 | 
			
		||||
      'moderation_state' => $field_value,
 | 
			
		||||
    ]);
 | 
			
		||||
    $entity->save();
 | 
			
		||||
 | 
			
		||||
    $field_output = $this->container->get('renderer')->executeInRenderContext(new RenderContext(), function () use ($entity, $formatter_settings) {
 | 
			
		||||
      return $entity->moderation_state->view($formatter_settings);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    $this->assertEquals($expected_output, $field_output[0]);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Test cases for testStateFieldFormatter().
 | 
			
		||||
   */
 | 
			
		||||
  public static function formatterTestCases() {
 | 
			
		||||
    return [
 | 
			
		||||
      'Draft State' => [
 | 
			
		||||
        'draft',
 | 
			
		||||
        [
 | 
			
		||||
          'type' => 'content_moderation_state',
 | 
			
		||||
          'settings' => [],
 | 
			
		||||
        ],
 | 
			
		||||
        [
 | 
			
		||||
          '#markup' => 'Draft',
 | 
			
		||||
        ],
 | 
			
		||||
      ],
 | 
			
		||||
      'Published State' => [
 | 
			
		||||
        'published',
 | 
			
		||||
        [
 | 
			
		||||
          'type' => 'content_moderation_state',
 | 
			
		||||
          'settings' => [],
 | 
			
		||||
        ],
 | 
			
		||||
        [
 | 
			
		||||
          '#markup' => 'Published',
 | 
			
		||||
        ],
 | 
			
		||||
      ],
 | 
			
		||||
    ];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,88 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\content_moderation\Kernel;
 | 
			
		||||
 | 
			
		||||
use Drupal\node\Entity\Node;
 | 
			
		||||
use Drupal\node\Entity\NodeType;
 | 
			
		||||
use Drupal\Tests\content_moderation\Traits\ContentModerationTestTrait;
 | 
			
		||||
use Drupal\Tests\views\Kernel\ViewsKernelTestBase;
 | 
			
		||||
use Drupal\views\Views;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests the views integration of content_moderation.
 | 
			
		||||
 *
 | 
			
		||||
 * @group content_moderation
 | 
			
		||||
 */
 | 
			
		||||
class ViewsDataIntegrationTest extends ViewsKernelTestBase {
 | 
			
		||||
 | 
			
		||||
  use ContentModerationTestTrait;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = [
 | 
			
		||||
    'content_moderation_test_views',
 | 
			
		||||
    'node',
 | 
			
		||||
    'content_moderation',
 | 
			
		||||
    'workflows',
 | 
			
		||||
    'entity_test',
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp($import_test_views = TRUE): void {
 | 
			
		||||
    parent::setUp($import_test_views);
 | 
			
		||||
 | 
			
		||||
    $this->installEntitySchema('node');
 | 
			
		||||
    $this->installEntitySchema('entity_test_mulrevpub');
 | 
			
		||||
    $this->installEntitySchema('user');
 | 
			
		||||
    $this->installEntitySchema('content_moderation_state');
 | 
			
		||||
    $this->installSchema('node', 'node_access');
 | 
			
		||||
    $this->installConfig('content_moderation');
 | 
			
		||||
 | 
			
		||||
    $node_type = NodeType::create([
 | 
			
		||||
      'type' => 'page',
 | 
			
		||||
      'name' => 'Page',
 | 
			
		||||
    ]);
 | 
			
		||||
    $node_type->save();
 | 
			
		||||
    $workflow = $this->createEditorialWorkflow();
 | 
			
		||||
    $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'page');
 | 
			
		||||
    $workflow->getTypePlugin()->addEntityTypeAndBundle('entity_test_mulrevpub', 'entity_test_mulrevpub');
 | 
			
		||||
    $workflow->save();
 | 
			
		||||
 | 
			
		||||
    $this->installConfig('content_moderation_test_views');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the content moderation state views field.
 | 
			
		||||
   */
 | 
			
		||||
  public function testContentModerationStateField(): void {
 | 
			
		||||
    $node = Node::create([
 | 
			
		||||
      'type' => 'page',
 | 
			
		||||
      'title' => 'Test title',
 | 
			
		||||
    ]);
 | 
			
		||||
    $node->moderation_state->value = 'published';
 | 
			
		||||
    $node->save();
 | 
			
		||||
 | 
			
		||||
    $view = Views::getView('test_content_moderation_field_state_test');
 | 
			
		||||
    $view->execute();
 | 
			
		||||
 | 
			
		||||
    $expected_result = [
 | 
			
		||||
      [
 | 
			
		||||
        'title' => 'Test title',
 | 
			
		||||
        'moderation_state' => 'published',
 | 
			
		||||
      ],
 | 
			
		||||
    ];
 | 
			
		||||
    $this->assertIdenticalResultset(
 | 
			
		||||
      $view,
 | 
			
		||||
      $expected_result,
 | 
			
		||||
      [
 | 
			
		||||
        'title' => 'title',
 | 
			
		||||
        'moderation_state' => 'moderation_state',
 | 
			
		||||
      ]);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,414 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\content_moderation\Kernel;
 | 
			
		||||
 | 
			
		||||
use Drupal\entity_test\Entity\EntityTestNoBundle;
 | 
			
		||||
use Drupal\language\Entity\ConfigurableLanguage;
 | 
			
		||||
use Drupal\node\Entity\Node;
 | 
			
		||||
use Drupal\node\Entity\NodeType;
 | 
			
		||||
use Drupal\Tests\content_moderation\Traits\ContentModerationTestTrait;
 | 
			
		||||
use Drupal\Tests\user\Traits\UserCreationTrait;
 | 
			
		||||
use Drupal\Tests\views\Kernel\ViewsKernelTestBase;
 | 
			
		||||
use Drupal\views\Views;
 | 
			
		||||
use Drupal\workflows\Entity\Workflow;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests the views 'moderation_state_filter' filter plugin.
 | 
			
		||||
 *
 | 
			
		||||
 * @coversDefaultClass \Drupal\content_moderation\Plugin\views\filter\ModerationStateFilter
 | 
			
		||||
 *
 | 
			
		||||
 * @group content_moderation
 | 
			
		||||
 */
 | 
			
		||||
class ViewsModerationStateFilterTest extends ViewsKernelTestBase {
 | 
			
		||||
 | 
			
		||||
  use ContentModerationTestTrait;
 | 
			
		||||
  use UserCreationTrait;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = [
 | 
			
		||||
    'content_moderation_test_views',
 | 
			
		||||
    'node',
 | 
			
		||||
    'content_moderation',
 | 
			
		||||
    'workflows',
 | 
			
		||||
    'workflow_type_test',
 | 
			
		||||
    'entity_test',
 | 
			
		||||
    'language',
 | 
			
		||||
    'content_translation',
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp($import_test_views = TRUE): void {
 | 
			
		||||
    parent::setUp(FALSE);
 | 
			
		||||
 | 
			
		||||
    $this->installEntitySchema('user');
 | 
			
		||||
    $this->installEntitySchema('node');
 | 
			
		||||
    $this->installEntitySchema('content_moderation_state');
 | 
			
		||||
    $this->installEntitySchema('entity_test_no_bundle');
 | 
			
		||||
    $this->installSchema('node', 'node_access');
 | 
			
		||||
    $this->installConfig('content_moderation');
 | 
			
		||||
 | 
			
		||||
    $node_type = NodeType::create([
 | 
			
		||||
      'type' => 'example',
 | 
			
		||||
      'name' => 'Example',
 | 
			
		||||
    ]);
 | 
			
		||||
    $node_type->save();
 | 
			
		||||
 | 
			
		||||
    $node_type = NodeType::create([
 | 
			
		||||
      'type' => 'another_example',
 | 
			
		||||
      'name' => 'Another Example',
 | 
			
		||||
    ]);
 | 
			
		||||
    $node_type->save();
 | 
			
		||||
 | 
			
		||||
    $node_type = NodeType::create([
 | 
			
		||||
      'type' => 'example_non_moderated',
 | 
			
		||||
      'name' => 'Non-Moderated Example',
 | 
			
		||||
    ]);
 | 
			
		||||
    $node_type->save();
 | 
			
		||||
 | 
			
		||||
    $workflow = $this->createEditorialWorkflow();
 | 
			
		||||
    $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example');
 | 
			
		||||
    $workflow->save();
 | 
			
		||||
 | 
			
		||||
    // Install the test views after moderation has been enabled on the example
 | 
			
		||||
    // bundle, so the moderation_state field exists.
 | 
			
		||||
    $this->installConfig('content_moderation_test_views');
 | 
			
		||||
 | 
			
		||||
    ConfigurableLanguage::createFromLangcode('fr')->save();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the content moderation state filter.
 | 
			
		||||
   */
 | 
			
		||||
  public function testStateFilterViewsRelationship(): void {
 | 
			
		||||
    $workflow = Workflow::load('editorial');
 | 
			
		||||
    $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example');
 | 
			
		||||
    $workflow->getTypePlugin()->addState('translated_draft', 'Bar');
 | 
			
		||||
    $configuration = $workflow->getTypePlugin()->getConfiguration();
 | 
			
		||||
    $configuration['states']['translated_draft'] += [
 | 
			
		||||
      'published' => FALSE,
 | 
			
		||||
      'default_revision' => FALSE,
 | 
			
		||||
    ];
 | 
			
		||||
    $workflow->getTypePlugin()->setConfiguration($configuration);
 | 
			
		||||
    $workflow->save();
 | 
			
		||||
 | 
			
		||||
    // Create a published default revision and one forward draft revision.
 | 
			
		||||
    $node = Node::create([
 | 
			
		||||
      'type' => 'example',
 | 
			
		||||
      'title' => 'Test Node',
 | 
			
		||||
      'moderation_state' => 'published',
 | 
			
		||||
    ]);
 | 
			
		||||
    $node->save();
 | 
			
		||||
    $node->setNewRevision();
 | 
			
		||||
    $node->moderation_state = 'draft';
 | 
			
		||||
    $node->save();
 | 
			
		||||
 | 
			
		||||
    // Create a draft default revision.
 | 
			
		||||
    $second_node = Node::create([
 | 
			
		||||
      'type' => 'example',
 | 
			
		||||
      'title' => 'Second Node',
 | 
			
		||||
      'moderation_state' => 'draft',
 | 
			
		||||
    ]);
 | 
			
		||||
    $second_node->save();
 | 
			
		||||
 | 
			
		||||
    // Create a published default revision.
 | 
			
		||||
    $third_node = Node::create([
 | 
			
		||||
      'type' => 'example',
 | 
			
		||||
      'title' => 'Third node',
 | 
			
		||||
      'moderation_state' => 'published',
 | 
			
		||||
    ]);
 | 
			
		||||
    $third_node->save();
 | 
			
		||||
 | 
			
		||||
    // Add a non-moderated node.
 | 
			
		||||
    $fourth_node = Node::create([
 | 
			
		||||
      'type' => 'example_non_moderated',
 | 
			
		||||
      'title' => 'Fourth node',
 | 
			
		||||
    ]);
 | 
			
		||||
    $fourth_node->save();
 | 
			
		||||
 | 
			
		||||
    // Create a translated published revision.
 | 
			
		||||
    $translated_forward_revision = $third_node->addTranslation('fr');
 | 
			
		||||
    $translated_forward_revision->title = 'Translated Node';
 | 
			
		||||
    $translated_forward_revision->setNewRevision(TRUE);
 | 
			
		||||
    $translated_forward_revision->moderation_state = 'translated_draft';
 | 
			
		||||
    $translated_forward_revision->save();
 | 
			
		||||
 | 
			
		||||
    // Test the filter within an AND filter group (the default) and an OR filter
 | 
			
		||||
    // group.
 | 
			
		||||
    $base_table_views = [
 | 
			
		||||
      'test_content_moderation_state_filter_base_table',
 | 
			
		||||
      'test_content_moderation_state_filter_base_table_filter_group_or',
 | 
			
		||||
    ];
 | 
			
		||||
    foreach ($base_table_views as $view_id) {
 | 
			
		||||
      // The three default revisions are listed when no filter is specified.
 | 
			
		||||
      $this->assertNodesWithFilters([$node, $second_node, $third_node], [], $view_id);
 | 
			
		||||
 | 
			
		||||
      // The default revision of node one and three are published.
 | 
			
		||||
      $this->assertNodesWithFilters([$node, $third_node], [
 | 
			
		||||
        'default_revision_state' => 'editorial-published',
 | 
			
		||||
      ], $view_id);
 | 
			
		||||
 | 
			
		||||
      // The default revision of node two is draft.
 | 
			
		||||
      $this->assertNodesWithFilters([$second_node], [
 | 
			
		||||
        'default_revision_state' => 'editorial-draft',
 | 
			
		||||
      ], $view_id);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Test the same three revisions on a view displaying content revisions.
 | 
			
		||||
    // Both nodes have one draft revision.
 | 
			
		||||
    $this->assertNodesWithFilters([$node, $second_node], [
 | 
			
		||||
      'moderation_state' => 'editorial-draft',
 | 
			
		||||
    ], 'test_content_moderation_state_filter_revision_table');
 | 
			
		||||
    // Creating a new forward revision of node three, creates a second published
 | 
			
		||||
    // revision of the original language, hence there are two published
 | 
			
		||||
    // revisions of node three.
 | 
			
		||||
    $this->assertNodesWithFilters([$node, $third_node, $third_node], [
 | 
			
		||||
      'moderation_state' => 'editorial-published',
 | 
			
		||||
    ], 'test_content_moderation_state_filter_revision_table');
 | 
			
		||||
    // There is a single forward translated revision with a new state, which is
 | 
			
		||||
    // also filterable.
 | 
			
		||||
    $this->assertNodesWithFilters([$translated_forward_revision], [
 | 
			
		||||
      'moderation_state' => 'editorial-translated_draft',
 | 
			
		||||
    ], 'test_content_moderation_state_filter_revision_table');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the moderation filter with a non-translatable entity type.
 | 
			
		||||
   */
 | 
			
		||||
  public function testNonTranslatableEntityType(): void {
 | 
			
		||||
    $workflow = Workflow::load('editorial');
 | 
			
		||||
    $workflow->getTypePlugin()->addEntityTypeAndBundle('entity_test_no_bundle', 'entity_test_no_bundle');
 | 
			
		||||
    $workflow->save();
 | 
			
		||||
 | 
			
		||||
    $test_entity = EntityTestNoBundle::create([
 | 
			
		||||
      'moderation_state' => 'draft',
 | 
			
		||||
    ]);
 | 
			
		||||
    $test_entity->save();
 | 
			
		||||
 | 
			
		||||
    $view = Views::getView('test_content_moderation_state_filter_entity_test');
 | 
			
		||||
    $view->setExposedInput([
 | 
			
		||||
      'moderation_state' => 'editorial-draft',
 | 
			
		||||
    ]);
 | 
			
		||||
    $view->execute();
 | 
			
		||||
    $this->assertIdenticalResultset($view, [['id' => $test_entity->id()]], ['id' => 'id']);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the moderation state filter on an entity added via a relationship.
 | 
			
		||||
   */
 | 
			
		||||
  public function testModerationStateFilterOnJoinedEntity(): void {
 | 
			
		||||
    $workflow = Workflow::load('editorial');
 | 
			
		||||
    $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example');
 | 
			
		||||
    $workflow->save();
 | 
			
		||||
 | 
			
		||||
    // Create some sample content that will satisfy a view of users with a
 | 
			
		||||
    // relationship to an item of content.
 | 
			
		||||
    $user = $this->createUser([], 'Test user');
 | 
			
		||||
    $node = Node::create([
 | 
			
		||||
      'type' => 'example',
 | 
			
		||||
      'title' => 'Test node',
 | 
			
		||||
      'moderation_state' => 'published',
 | 
			
		||||
      'uid' => $user->id(),
 | 
			
		||||
    ]);
 | 
			
		||||
    $node->save();
 | 
			
		||||
 | 
			
		||||
    // When filtering by published nodes, the sample content will appear.
 | 
			
		||||
    $view = Views::getView('test_content_moderation_filter_via_relationship');
 | 
			
		||||
    $view->setExposedInput([
 | 
			
		||||
      'moderation_state' => 'editorial-published',
 | 
			
		||||
    ]);
 | 
			
		||||
    $view->execute();
 | 
			
		||||
    $this->assertIdenticalResultset($view, [
 | 
			
		||||
      [
 | 
			
		||||
        'name' => 'Test user',
 | 
			
		||||
        'title' => 'Test node',
 | 
			
		||||
        'moderation_state' => 'published',
 | 
			
		||||
      ],
 | 
			
		||||
    ], [
 | 
			
		||||
      'name' => 'name',
 | 
			
		||||
      'title' => 'title',
 | 
			
		||||
      'moderation_state' => 'moderation_state',
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    // Filtering by the draft state will filter out the sample content.
 | 
			
		||||
    $view = Views::getView('test_content_moderation_filter_via_relationship');
 | 
			
		||||
    $view->setExposedInput([
 | 
			
		||||
      'moderation_state' => 'editorial-draft',
 | 
			
		||||
    ]);
 | 
			
		||||
    $view->execute();
 | 
			
		||||
    $this->assertIdenticalResultset($view, [], ['name' => 'name']);
 | 
			
		||||
 | 
			
		||||
    // Revision Data Table Relationship: Filtering by the published state will
 | 
			
		||||
    // filter out the sample content.
 | 
			
		||||
    $view = Views::getView('test_content_moderation_filter_via_revision_relationship');
 | 
			
		||||
    $view->setExposedInput([
 | 
			
		||||
      'moderation_state' => 'editorial-published',
 | 
			
		||||
    ]);
 | 
			
		||||
    $view->execute();
 | 
			
		||||
    $this->assertIdenticalResultset($view, [
 | 
			
		||||
      [
 | 
			
		||||
        'name' => 'Test user',
 | 
			
		||||
        'title' => 'Test node',
 | 
			
		||||
        'moderation_state' => 'published',
 | 
			
		||||
      ],
 | 
			
		||||
    ], [
 | 
			
		||||
      'name' => 'name',
 | 
			
		||||
      'title' => 'title',
 | 
			
		||||
      'moderation_state' => 'moderation_state',
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    // Revision Data Table Relationship: Filtering by the draft state will
 | 
			
		||||
    // filter out the sample content.
 | 
			
		||||
    $view = Views::getView('test_content_moderation_filter_via_revision_relationship');
 | 
			
		||||
    $view->setExposedInput([
 | 
			
		||||
      'moderation_state' => 'editorial-draft',
 | 
			
		||||
    ]);
 | 
			
		||||
    $view->execute();
 | 
			
		||||
    $this->assertIdenticalResultset($view, [], ['name' => 'name']);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the list of states in the filter plugin.
 | 
			
		||||
   */
 | 
			
		||||
  public function testStateFilterStatesList(): void {
 | 
			
		||||
    // By default a view of nodes will not have states to filter.
 | 
			
		||||
    $workflow = Workflow::load('editorial');
 | 
			
		||||
    $workflow->getTypePlugin()->removeEntityTypeAndBundle('node', 'example');
 | 
			
		||||
    $workflow->save();
 | 
			
		||||
    $this->assertPluginStates([]);
 | 
			
		||||
 | 
			
		||||
    // Adding a content type to the editorial workflow will enable all of the
 | 
			
		||||
    // editorial states.
 | 
			
		||||
    $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example');
 | 
			
		||||
    $workflow->save();
 | 
			
		||||
    $this->assertPluginStates([
 | 
			
		||||
      'Editorial' => [
 | 
			
		||||
        'editorial-draft' => 'Draft',
 | 
			
		||||
        'editorial-published' => 'Published',
 | 
			
		||||
        'editorial-archived' => 'Archived',
 | 
			
		||||
      ],
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    // Adding a workflow which is not content moderation will not add any
 | 
			
		||||
    // additional states to the views filter.
 | 
			
		||||
    $workflow = Workflow::create([
 | 
			
		||||
      'id' => 'test',
 | 
			
		||||
      'label' => 'Test',
 | 
			
		||||
      'type' => 'workflow_type_complex_test',
 | 
			
		||||
    ]);
 | 
			
		||||
    $workflow->getTypePlugin()->addState('draft', 'Draft');
 | 
			
		||||
    $workflow->save();
 | 
			
		||||
    $this->assertPluginStates([
 | 
			
		||||
      'Editorial' => [
 | 
			
		||||
        'editorial-draft' => 'Draft',
 | 
			
		||||
        'editorial-published' => 'Published',
 | 
			
		||||
        'editorial-archived' => 'Archived',
 | 
			
		||||
      ],
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    // Adding a new content moderation workflow will add additional states to
 | 
			
		||||
    // filter.
 | 
			
		||||
    $workflow = Workflow::create([
 | 
			
		||||
      'id' => 'moderation_test',
 | 
			
		||||
      'type' => 'content_moderation',
 | 
			
		||||
      'label' => 'Moderation test',
 | 
			
		||||
    ]);
 | 
			
		||||
    $workflow->getTypePlugin()->addState('foo', 'Foo State');
 | 
			
		||||
    $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example');
 | 
			
		||||
    $workflow->save();
 | 
			
		||||
    $this->assertPluginStates([
 | 
			
		||||
      'Editorial' => [
 | 
			
		||||
        'editorial-draft' => 'Draft',
 | 
			
		||||
        'editorial-published' => 'Published',
 | 
			
		||||
        'editorial-archived' => 'Archived',
 | 
			
		||||
      ],
 | 
			
		||||
      'Moderation test' => [
 | 
			
		||||
        'moderation_test-foo' => 'Foo State',
 | 
			
		||||
        'moderation_test-draft' => 'Draft',
 | 
			
		||||
        'moderation_test-published' => 'Published',
 | 
			
		||||
      ],
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    // Deleting a workflow will remove the states from the filter.
 | 
			
		||||
    $workflow = Workflow::load('moderation_test');
 | 
			
		||||
    $workflow->delete();
 | 
			
		||||
    $this->assertPluginStates([
 | 
			
		||||
      'Editorial' => [
 | 
			
		||||
        'editorial-draft' => 'Draft',
 | 
			
		||||
        'editorial-published' => 'Published',
 | 
			
		||||
        'editorial-archived' => 'Archived',
 | 
			
		||||
      ],
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    // Deleting a state from a workflow will remove the state from the filter.
 | 
			
		||||
    $workflow = Workflow::load('editorial');
 | 
			
		||||
    $workflow->getTypePlugin()->deleteState('archived');
 | 
			
		||||
    $workflow->save();
 | 
			
		||||
    $this->assertPluginStates([
 | 
			
		||||
      'Editorial' => [
 | 
			
		||||
        'editorial-draft' => 'Draft',
 | 
			
		||||
        'editorial-published' => 'Published',
 | 
			
		||||
      ],
 | 
			
		||||
    ]);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Assert the plugin states.
 | 
			
		||||
   *
 | 
			
		||||
   * @param string[] $states
 | 
			
		||||
   *   The states which should appear in the filter.
 | 
			
		||||
   *
 | 
			
		||||
   * @internal
 | 
			
		||||
   */
 | 
			
		||||
  protected function assertPluginStates(array $states): void {
 | 
			
		||||
    $plugin = Views::pluginManager('filter')->createInstance('moderation_state_filter', []);
 | 
			
		||||
    $view = Views::getView('test_content_moderation_state_filter_base_table');
 | 
			
		||||
    $plugin->init($view, $view->getDisplay());
 | 
			
		||||
    $this->assertEquals($states, $plugin->getValueOptions());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Assert the nodes appear when the test view is executed.
 | 
			
		||||
   *
 | 
			
		||||
   * @param \Drupal\node\NodeInterface[] $nodes
 | 
			
		||||
   *   Nodes to assert are in the views result.
 | 
			
		||||
   * @param array $filters
 | 
			
		||||
   *   An array of filters to apply to the view.
 | 
			
		||||
   * @param string $view_id
 | 
			
		||||
   *   The view to execute for the results.
 | 
			
		||||
   *
 | 
			
		||||
   * @internal
 | 
			
		||||
   */
 | 
			
		||||
  protected function assertNodesWithFilters(array $nodes, array $filters, string $view_id = 'test_content_moderation_state_filter_base_table'): void {
 | 
			
		||||
    $view = Views::getView($view_id);
 | 
			
		||||
    $view->setExposedInput($filters);
 | 
			
		||||
    $view->execute();
 | 
			
		||||
 | 
			
		||||
    // Verify the join configuration.
 | 
			
		||||
    $query = $view->getQuery();
 | 
			
		||||
    $join = $query->getTableInfo('content_moderation_state')['join'];
 | 
			
		||||
    $configuration = $join->configuration;
 | 
			
		||||
    $this->assertEquals('content_moderation_state_field_revision', $configuration['table']);
 | 
			
		||||
    $this->assertEquals('content_entity_revision_id', $configuration['field']);
 | 
			
		||||
    $this->assertEquals('vid', $configuration['left_field']);
 | 
			
		||||
    $this->assertEquals('content_entity_type_id', $configuration['extra'][0]['field']);
 | 
			
		||||
    $this->assertEquals('node', $configuration['extra'][0]['value']);
 | 
			
		||||
 | 
			
		||||
    $this->assertEquals('content_entity_id', $configuration['extra'][1]['field']);
 | 
			
		||||
    $this->assertEquals('nid', $configuration['extra'][1]['left_field']);
 | 
			
		||||
    $this->assertEquals('langcode', $configuration['extra'][2]['field']);
 | 
			
		||||
    $this->assertEquals('langcode', $configuration['extra'][2]['left_field']);
 | 
			
		||||
 | 
			
		||||
    $expected_result = [];
 | 
			
		||||
    foreach ($nodes as $node) {
 | 
			
		||||
      $expected_result[] = ['nid' => $node->id()];
 | 
			
		||||
    }
 | 
			
		||||
    $this->assertIdenticalResultset($view, $expected_result, ['nid' => 'nid']);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,196 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\content_moderation\Kernel;
 | 
			
		||||
 | 
			
		||||
use Drupal\language\Entity\ConfigurableLanguage;
 | 
			
		||||
use Drupal\node\Entity\Node;
 | 
			
		||||
use Drupal\node\Entity\NodeType;
 | 
			
		||||
use Drupal\Tests\content_moderation\Traits\ContentModerationTestTrait;
 | 
			
		||||
use Drupal\Tests\views\Kernel\ViewsKernelTestBase;
 | 
			
		||||
use Drupal\views\Views;
 | 
			
		||||
use Symfony\Component\HttpFoundation\Request;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests the views moderation_state field sorting integration.
 | 
			
		||||
 *
 | 
			
		||||
 * @group content_moderation
 | 
			
		||||
 */
 | 
			
		||||
class ViewsModerationStateSortTest extends ViewsKernelTestBase {
 | 
			
		||||
 | 
			
		||||
  use ContentModerationTestTrait;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = [
 | 
			
		||||
    'node',
 | 
			
		||||
    'content_moderation',
 | 
			
		||||
    'workflows',
 | 
			
		||||
    'workflow_type_test',
 | 
			
		||||
    'entity_test',
 | 
			
		||||
    'language',
 | 
			
		||||
    'content_translation',
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp($import_test_views = TRUE): void {
 | 
			
		||||
    parent::setUp(FALSE);
 | 
			
		||||
 | 
			
		||||
    $this->installEntitySchema('user');
 | 
			
		||||
    $this->installEntitySchema('node');
 | 
			
		||||
    $this->installEntitySchema('content_moderation_state');
 | 
			
		||||
    $this->installEntitySchema('entity_test_no_bundle');
 | 
			
		||||
    $this->installSchema('node', 'node_access');
 | 
			
		||||
    $this->installConfig('content_moderation');
 | 
			
		||||
 | 
			
		||||
    $node_type = NodeType::create([
 | 
			
		||||
      'type' => 'example',
 | 
			
		||||
      'name' => 'Example',
 | 
			
		||||
    ]);
 | 
			
		||||
    $node_type->save();
 | 
			
		||||
 | 
			
		||||
    ConfigurableLanguage::createFromLangcode('fr')->save();
 | 
			
		||||
 | 
			
		||||
    $workflow = $this->createEditorialWorkflow();
 | 
			
		||||
    $workflow->getTypePlugin()->addState('zz_draft', 'ZZ Draft');
 | 
			
		||||
    $workflow->getTypePlugin()->addState('aa_draft', 'AA Draft');
 | 
			
		||||
    $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example');
 | 
			
		||||
    $workflow->save();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests sorting with a standard data base table.
 | 
			
		||||
   */
 | 
			
		||||
  public function testSortBaseTable(): void {
 | 
			
		||||
    $this->enableModules(['content_moderation_test_views']);
 | 
			
		||||
    $this->installConfig(['content_moderation_test_views']);
 | 
			
		||||
 | 
			
		||||
    // Create two revisions. The sorted revision will be 'zz_draft' since it
 | 
			
		||||
    // will be attached to the default revision of the entity.
 | 
			
		||||
    $first_node = Node::create([
 | 
			
		||||
      'type' => 'example',
 | 
			
		||||
      'title' => 'Foo',
 | 
			
		||||
      'moderation_state' => 'aa_draft',
 | 
			
		||||
    ]);
 | 
			
		||||
    $first_node->save();
 | 
			
		||||
    $first_node->moderation_state = 'zz_draft';
 | 
			
		||||
    $first_node->save();
 | 
			
		||||
 | 
			
		||||
    // Create a second published node, which falls between aa_draft and zz_draft
 | 
			
		||||
    // for the purposes of testing.
 | 
			
		||||
    $second_node = Node::create([
 | 
			
		||||
      'type' => 'example',
 | 
			
		||||
      'title' => 'Foo',
 | 
			
		||||
      'moderation_state' => 'published',
 | 
			
		||||
    ]);
 | 
			
		||||
    $second_node->save();
 | 
			
		||||
 | 
			
		||||
    // Ascending order will see 'published' followed by 'zz_draft'.
 | 
			
		||||
    $this->assertSortResults('test_content_moderation_state_sort_base_table', 'nid', 'ASC', [
 | 
			
		||||
      ['nid' => $second_node->id()],
 | 
			
		||||
      ['nid' => $first_node->id()],
 | 
			
		||||
    ]);
 | 
			
		||||
    // Descending will reverse the order.
 | 
			
		||||
    $this->assertSortResults('test_content_moderation_state_sort_base_table', 'nid', 'DESC', [
 | 
			
		||||
      ['nid' => $first_node->id()],
 | 
			
		||||
      ['nid' => $second_node->id()],
 | 
			
		||||
    ]);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests sorting with the revision base table.
 | 
			
		||||
   */
 | 
			
		||||
  public function testSortRevisionBaseTable(): void {
 | 
			
		||||
    $this->enableModules(['content_moderation_test_views']);
 | 
			
		||||
    $this->installConfig(['content_moderation_test_views']);
 | 
			
		||||
 | 
			
		||||
    // Create a series of node revisions in different states and store
 | 
			
		||||
    // each revision ID at the given state.
 | 
			
		||||
    $node = Node::create([
 | 
			
		||||
      'type' => 'example',
 | 
			
		||||
      'title' => 'Foo',
 | 
			
		||||
      'moderation_state' => 'published',
 | 
			
		||||
    ]);
 | 
			
		||||
    $node->save();
 | 
			
		||||
    $published_revision_id = $node->getRevisionId();
 | 
			
		||||
 | 
			
		||||
    $node->moderation_state = 'draft';
 | 
			
		||||
    $node->save();
 | 
			
		||||
    $draft_revision_id = $node->getRevisionId();
 | 
			
		||||
 | 
			
		||||
    $node->moderation_state = 'aa_draft';
 | 
			
		||||
    $node->save();
 | 
			
		||||
    $aa_draft_revision_id = $node->getRevisionId();
 | 
			
		||||
 | 
			
		||||
    $translated = $node->addTranslation('fr');
 | 
			
		||||
    $translated->moderation_state = 'zz_draft';
 | 
			
		||||
    $translated->title = 'Translated';
 | 
			
		||||
    $translated->save();
 | 
			
		||||
    $zz_draft_revision_id = $translated->getRevisionId();
 | 
			
		||||
 | 
			
		||||
    // A second aa_draft revision will be created for the non-translated
 | 
			
		||||
    // revision. Since in this case there will be two revisions with "aa_draft"
 | 
			
		||||
    // we add another sort in content_moderation_test_views_views_query_alter.
 | 
			
		||||
    // Secondary sorting is not an option in views when using exposed sorting
 | 
			
		||||
    // and table click sorting, so in order to maintain the same level of
 | 
			
		||||
    // coverage this is required.
 | 
			
		||||
    $second_aa_draft_revision_id = $translated->getRevisionId();
 | 
			
		||||
 | 
			
		||||
    $this->assertSortResults('test_content_moderation_state_sort_revision_table', 'vid', 'ASC', [
 | 
			
		||||
      ['vid' => $aa_draft_revision_id],
 | 
			
		||||
      ['vid' => $second_aa_draft_revision_id],
 | 
			
		||||
      ['vid' => $draft_revision_id],
 | 
			
		||||
      ['vid' => $published_revision_id],
 | 
			
		||||
      ['vid' => $zz_draft_revision_id],
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    $this->assertSortResults('test_content_moderation_state_sort_revision_table', 'vid', 'DESC', [
 | 
			
		||||
      ['vid' => $zz_draft_revision_id],
 | 
			
		||||
      ['vid' => $published_revision_id],
 | 
			
		||||
      ['vid' => $draft_revision_id],
 | 
			
		||||
      ['vid' => $aa_draft_revision_id],
 | 
			
		||||
      ['vid' => $second_aa_draft_revision_id],
 | 
			
		||||
    ]);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Assert the order of a views sort result.
 | 
			
		||||
   *
 | 
			
		||||
   * @param string $view_id
 | 
			
		||||
   *   The ID of the view.
 | 
			
		||||
   * @param string $column
 | 
			
		||||
   *   The column associated with each row.
 | 
			
		||||
   * @param string $order
 | 
			
		||||
   *   The sort order.
 | 
			
		||||
   * @param array $expected
 | 
			
		||||
   *   The expected results array.
 | 
			
		||||
   *
 | 
			
		||||
   * @internal
 | 
			
		||||
   */
 | 
			
		||||
  protected function assertSortResults(string $view_id, string $column, string $order, array $expected): void {
 | 
			
		||||
    // Test with exposed input.
 | 
			
		||||
    $view = Views::getView($view_id);
 | 
			
		||||
    $view->setExposedInput([
 | 
			
		||||
      'sort_by' => 'moderation_state',
 | 
			
		||||
      'sort_order' => $order,
 | 
			
		||||
    ]);
 | 
			
		||||
    $view->execute();
 | 
			
		||||
    $this->assertIdenticalResultset($view, $expected, [$column => $column]);
 | 
			
		||||
 | 
			
		||||
    // Test click sorting.
 | 
			
		||||
    $view = Views::getView($view_id);
 | 
			
		||||
    $view->removeHandler('default', 'sort', 'moderation_state');
 | 
			
		||||
    $request = new Request([
 | 
			
		||||
      'order' => 'moderation_state',
 | 
			
		||||
      'sort' => strtolower($order),
 | 
			
		||||
    ]);
 | 
			
		||||
    $view->setRequest($request);
 | 
			
		||||
    $view->execute();
 | 
			
		||||
    $this->assertIdenticalResultset($view, $expected, [$column => $column]);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,375 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\content_moderation\Kernel;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Entity\EntityInterface;
 | 
			
		||||
use Drupal\entity_test\Entity\EntityTestMulRevPub;
 | 
			
		||||
use Drupal\node\Entity\Node;
 | 
			
		||||
use Drupal\Tests\content_moderation\Traits\ContentModerationTestTrait;
 | 
			
		||||
use Drupal\Tests\node\Traits\ContentTypeCreationTrait;
 | 
			
		||||
use Drupal\Tests\user\Traits\UserCreationTrait;
 | 
			
		||||
use Drupal\Tests\workspaces\Kernel\WorkspaceTestTrait;
 | 
			
		||||
use Drupal\workflows\Entity\Workflow;
 | 
			
		||||
use Drupal\workflows\WorkflowInterface;
 | 
			
		||||
use Drupal\workspaces\WorkspacePublishException;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests that Workspaces and Content Moderation work together properly.
 | 
			
		||||
 *
 | 
			
		||||
 * @group content_moderation
 | 
			
		||||
 * @group workspaces
 | 
			
		||||
 * @group #slow
 | 
			
		||||
 */
 | 
			
		||||
class WorkspacesContentModerationStateTest extends ContentModerationStateTest {
 | 
			
		||||
 | 
			
		||||
  use ContentModerationTestTrait {
 | 
			
		||||
    createEditorialWorkflow as traitCreateEditorialWorkflow;
 | 
			
		||||
    addEntityTypeAndBundleToWorkflow as traitAddEntityTypeAndBundleToWorkflow;
 | 
			
		||||
  }
 | 
			
		||||
  use ContentTypeCreationTrait {
 | 
			
		||||
    createContentType as traitCreateContentType;
 | 
			
		||||
  }
 | 
			
		||||
  use UserCreationTrait;
 | 
			
		||||
  use WorkspaceTestTrait;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The ID of the revisionable entity type used in the tests.
 | 
			
		||||
   *
 | 
			
		||||
   * @var string
 | 
			
		||||
   */
 | 
			
		||||
  protected $revEntityTypeId = 'entity_test_revpub';
 | 
			
		||||
 | 
			
		||||
  const SKIP_METHODS = [
 | 
			
		||||
    // This test creates published default revisions in Live, which can not be
 | 
			
		||||
    // deleted in a workspace. A test scenario for the case when Content
 | 
			
		||||
    // Moderation and Workspaces are used together is covered in
 | 
			
		||||
    // parent::testContentModerationStateRevisionDataRemoval().
 | 
			
		||||
    'testContentModerationStateDataRemoval',
 | 
			
		||||
    // This test does not assert anything that can be workspace-specific.
 | 
			
		||||
    'testModerationWithFieldConfigOverride',
 | 
			
		||||
    // This test does not assert anything that can be workspace-specific.
 | 
			
		||||
    'testWorkflowDependencies',
 | 
			
		||||
    // This test does not assert anything that can be workspace-specific.
 | 
			
		||||
    'testWorkflowNonConfigBundleDependencies',
 | 
			
		||||
    // This test does not assert anything that can be workspace-specific.
 | 
			
		||||
    'testGetCurrentUserId',
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    if (in_array($this->name(), static::SKIP_METHODS, TRUE)) {
 | 
			
		||||
      $this->markTestSkipped('Irrelevant for this test');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
 | 
			
		||||
    $this->initializeWorkspacesModule();
 | 
			
		||||
    $this->switchToWorkspace('stage');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests that the 'workspace' entity type can not be moderated.
 | 
			
		||||
   *
 | 
			
		||||
   * @see \Drupal\workspaces\EntityTypeInfo::entityTypeAlter()
 | 
			
		||||
   */
 | 
			
		||||
  public function testWorkspaceEntityTypeModeration(): void {
 | 
			
		||||
    /** @var \Drupal\content_moderation\ModerationInformationInterface $moderation_info */
 | 
			
		||||
    $moderation_info = \Drupal::service('content_moderation.moderation_information');
 | 
			
		||||
    $entity_type = \Drupal::entityTypeManager()->getDefinition('workspace');
 | 
			
		||||
    $this->assertFalse($moderation_info->canModerateEntitiesOfEntityType($entity_type));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the integration between Content Moderation and Workspaces.
 | 
			
		||||
   */
 | 
			
		||||
  public function testContentModerationIntegrationWithWorkspaces(): void {
 | 
			
		||||
    $editorial = $this->createEditorialWorkflow();
 | 
			
		||||
    $access_handler = \Drupal::entityTypeManager()->getAccessControlHandler('workspace');
 | 
			
		||||
 | 
			
		||||
    // Create another workflow which has the same states as the 'editorial' one,
 | 
			
		||||
    // but it doesn't create default revisions for the 'archived' state. This
 | 
			
		||||
    // covers the case when two bundles of the same entity type use different
 | 
			
		||||
    // workflows with same moderation state names but with different settings.
 | 
			
		||||
    $editorial_2_values = $editorial->toArray();
 | 
			
		||||
    unset($editorial_2_values['uuid']);
 | 
			
		||||
    $editorial_2_values['id'] = 'editorial_2';
 | 
			
		||||
    $editorial_2_values['type_settings']['states']['archived']['default_revision'] = FALSE;
 | 
			
		||||
 | 
			
		||||
    $editorial_2 = Workflow::create($editorial_2_values);
 | 
			
		||||
    $this->workspaceManager->executeOutsideWorkspace(function () use ($editorial_2) {
 | 
			
		||||
      $editorial_2->save();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Create two bundles and assign the two workflows for each of them.
 | 
			
		||||
    $this->createContentType(['type' => 'page']);
 | 
			
		||||
    $this->addEntityTypeAndBundleToWorkflow($editorial, 'node', 'page');
 | 
			
		||||
    $this->createContentType(['type' => 'article']);
 | 
			
		||||
    $this->addEntityTypeAndBundleToWorkflow($editorial_2, 'node', 'article');
 | 
			
		||||
 | 
			
		||||
    // Create three entities for each bundle, covering all the available
 | 
			
		||||
    // moderation states.
 | 
			
		||||
    $page_archived = Node::create([
 | 
			
		||||
      'type' => 'page',
 | 
			
		||||
      'title' => 'Test page - archived',
 | 
			
		||||
      'moderation_state' => 'archived',
 | 
			
		||||
    ]);
 | 
			
		||||
    $page_archived->save();
 | 
			
		||||
    $page_draft = Node::create([
 | 
			
		||||
      'type' => 'page',
 | 
			
		||||
      'title' => 'Test page - draft',
 | 
			
		||||
      'moderation_state' => 'draft',
 | 
			
		||||
    ]);
 | 
			
		||||
    $page_draft->save();
 | 
			
		||||
    $page_published = Node::create([
 | 
			
		||||
      'type' => 'page',
 | 
			
		||||
      'title' => 'Test page - published',
 | 
			
		||||
      'moderation_state' => 'published',
 | 
			
		||||
    ]);
 | 
			
		||||
    $page_published->save();
 | 
			
		||||
 | 
			
		||||
    $article_archived = Node::create([
 | 
			
		||||
      'type' => 'article',
 | 
			
		||||
      'title' => 'Test article - archived',
 | 
			
		||||
      'moderation_state' => 'archived',
 | 
			
		||||
    ]);
 | 
			
		||||
    $article_archived->save();
 | 
			
		||||
    $article_draft = Node::create([
 | 
			
		||||
      'type' => 'article',
 | 
			
		||||
      'title' => 'Test article - draft',
 | 
			
		||||
      'moderation_state' => 'draft',
 | 
			
		||||
    ]);
 | 
			
		||||
    $article_draft->save();
 | 
			
		||||
    $article_published = Node::create([
 | 
			
		||||
      'type' => 'article',
 | 
			
		||||
      'title' => 'Test article - published',
 | 
			
		||||
      'moderation_state' => 'published',
 | 
			
		||||
    ]);
 | 
			
		||||
    $article_published->save();
 | 
			
		||||
 | 
			
		||||
    // We have three items in a non-default moderation state:
 | 
			
		||||
    // - $page_draft
 | 
			
		||||
    // - $article_archived
 | 
			
		||||
    // - $article_draft
 | 
			
		||||
    // Therefore the workspace can not be published.
 | 
			
		||||
    // This assertion also covers two moderation states from different workflows
 | 
			
		||||
    // with the same name ('archived'), but with different default revision
 | 
			
		||||
    // settings.
 | 
			
		||||
    try {
 | 
			
		||||
      $this->workspaces['stage']->publish();
 | 
			
		||||
      $this->fail('The expected exception was not thrown.');
 | 
			
		||||
    }
 | 
			
		||||
    catch (WorkspacePublishException $e) {
 | 
			
		||||
      $this->assertEquals('The Stage workspace can not be published because it contains 3 items in an unpublished moderation state.', $e->getMessage());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Get the $page_draft node to a publishable state and try again.
 | 
			
		||||
    $page_draft->moderation_state->value = 'published';
 | 
			
		||||
    $page_draft->save();
 | 
			
		||||
    try {
 | 
			
		||||
      $access_handler->resetCache();
 | 
			
		||||
      $this->workspaces['stage']->publish();
 | 
			
		||||
      $this->fail('The expected exception was not thrown.');
 | 
			
		||||
    }
 | 
			
		||||
    catch (WorkspacePublishException $e) {
 | 
			
		||||
      $this->assertEquals('The Stage workspace can not be published because it contains 2 items in an unpublished moderation state.', $e->getMessage());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Get the $article_archived node to a publishable state and try again.
 | 
			
		||||
    $article_archived->moderation_state->value = 'published';
 | 
			
		||||
    $article_archived->save();
 | 
			
		||||
    try {
 | 
			
		||||
      $access_handler->resetCache();
 | 
			
		||||
      $this->workspaces['stage']->publish();
 | 
			
		||||
      $this->fail('The expected exception was not thrown.');
 | 
			
		||||
    }
 | 
			
		||||
    catch (WorkspacePublishException $e) {
 | 
			
		||||
      $this->assertEquals('The Stage workspace can not be published because it contains 1 item in an unpublished moderation state.', $e->getMessage());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Get the $article_draft node to a publishable state and try again.
 | 
			
		||||
    $article_draft->moderation_state->value = 'published';
 | 
			
		||||
    $article_draft->save();
 | 
			
		||||
    $access_handler->resetCache();
 | 
			
		||||
    $this->workspaces['stage']->publish();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Publish a workspace with workflows including no tracked default revisions.
 | 
			
		||||
   */
 | 
			
		||||
  public function testContentModerationWithoutDefaultRevisionsInWorkspaces(): void {
 | 
			
		||||
    $access_handler = $this->container->get('entity_type.manager')->getAccessControlHandler('workspace');
 | 
			
		||||
    // Create a workflow which has the same states as the 'editorial' one,
 | 
			
		||||
    // but it doesn't create any default revisions. This covers the case when a
 | 
			
		||||
    // workspace is published containing no tracked types. This has to be the
 | 
			
		||||
    // only workflow.
 | 
			
		||||
    $editorial = $this->createEditorialWorkflow();
 | 
			
		||||
    $type_settings = $editorial->get('type_settings');
 | 
			
		||||
    $type_settings['states']['draft']['default_revision'] = FALSE;
 | 
			
		||||
    $type_settings['states']['archived']['default_revision'] = FALSE;
 | 
			
		||||
    $this->workspaceManager->executeOutsideWorkspace(function () use ($editorial) {
 | 
			
		||||
      $editorial->save();
 | 
			
		||||
    });
 | 
			
		||||
    // Create an node bundle 'note' that uses non-default workflow.
 | 
			
		||||
    $this->createContentType(['type' => 'note']);
 | 
			
		||||
 | 
			
		||||
    // Create content in all states none with default revisions.
 | 
			
		||||
    $note_archived = Node::create([
 | 
			
		||||
      'type' => 'note',
 | 
			
		||||
      'title' => 'Test note - archived',
 | 
			
		||||
      'moderation_state' => 'archived',
 | 
			
		||||
    ]);
 | 
			
		||||
    $note_archived->save();
 | 
			
		||||
    $note_draft = Node::create([
 | 
			
		||||
      'type' => 'note',
 | 
			
		||||
      'title' => 'Test note - draft',
 | 
			
		||||
      'moderation_state' => 'draft',
 | 
			
		||||
    ]);
 | 
			
		||||
    $note_draft->save();
 | 
			
		||||
    $note_published = Node::create([
 | 
			
		||||
      'type' => 'note',
 | 
			
		||||
      'title' => 'Test note - published',
 | 
			
		||||
      'moderation_state' => 'published',
 | 
			
		||||
    ]);
 | 
			
		||||
    $note_published->save();
 | 
			
		||||
 | 
			
		||||
    // Check workspace can be published.
 | 
			
		||||
    $access_handler->resetCache();
 | 
			
		||||
    $this->workspaces['stage']->publish();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Publish a workspace with multiple entities from different entity types.
 | 
			
		||||
   */
 | 
			
		||||
  public function testContentModerationMultipleEntityTypesWithWorkspaces(): void {
 | 
			
		||||
    $editorial = $this->createEditorialWorkflow();
 | 
			
		||||
    $this->createContentType(['type' => 'page']);
 | 
			
		||||
    $this->addEntityTypeAndBundleToWorkflow($editorial, 'node', 'page');
 | 
			
		||||
    $this->addEntityTypeAndBundleToWorkflow($editorial, 'entity_test_mulrevpub', 'entity_test_mulrevpub');
 | 
			
		||||
 | 
			
		||||
    // Create an entity with a previous revision that is tracked in unpublished
 | 
			
		||||
    // state.
 | 
			
		||||
    $entity_with_revision = EntityTestMulRevPub::create([
 | 
			
		||||
      'title' => 'Test entity mulrevpub',
 | 
			
		||||
      'type' => 'entity_test_mulrevpub',
 | 
			
		||||
      'moderation_state' => 'draft',
 | 
			
		||||
    ]);
 | 
			
		||||
    $entity_with_revision->save();
 | 
			
		||||
    $entity_with_revision->save();
 | 
			
		||||
    $entity_with_revision = $this->reloadEntity($entity_with_revision);
 | 
			
		||||
    // Confirm unpublished earlier revision.
 | 
			
		||||
    $this->assertEquals('draft', $entity_with_revision->moderation_state->value);
 | 
			
		||||
    $earlier_revision_id = $entity_with_revision->getRevisionId();
 | 
			
		||||
    // Publish.
 | 
			
		||||
    $entity_with_revision->moderation_state->value = 'published';
 | 
			
		||||
    $entity_with_revision->save();
 | 
			
		||||
    $entity_with_revision = $this->reloadEntity($entity_with_revision);
 | 
			
		||||
    // Confirm publish revision.
 | 
			
		||||
    $this->assertEquals('published', $entity_with_revision->moderation_state->value);
 | 
			
		||||
    $published_revision_id = $entity_with_revision->getRevisionId();
 | 
			
		||||
    $this->assertNotEquals($earlier_revision_id, $published_revision_id);
 | 
			
		||||
 | 
			
		||||
    // Create an entity that has a default revision id the same as the previous
 | 
			
		||||
    // entity's old revision.
 | 
			
		||||
    $entity_without_revision = Node::create([
 | 
			
		||||
      'title' => 'Test node page',
 | 
			
		||||
      'type' => 'page',
 | 
			
		||||
      'moderation_state' => 'published',
 | 
			
		||||
    ]);
 | 
			
		||||
    $entity_without_revision->save();
 | 
			
		||||
    $entity_without_revision = $this->reloadEntity($entity_without_revision);
 | 
			
		||||
    $this->assertEquals('published', $entity_without_revision->moderation_state->value);
 | 
			
		||||
 | 
			
		||||
    // Current published revisions of second entity has the same revision as
 | 
			
		||||
    // earlier unpublished revision of first entity.
 | 
			
		||||
    $this->assertEquals($entity_without_revision->getRevisionId(), $earlier_revision_id);
 | 
			
		||||
    $this->workspaces['stage']->publish();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Test cases for basic moderation test.
 | 
			
		||||
   */
 | 
			
		||||
  public static function basicModerationTestCases() {
 | 
			
		||||
    return [
 | 
			
		||||
      'Nodes' => [
 | 
			
		||||
        'node',
 | 
			
		||||
      ],
 | 
			
		||||
      'Block content' => [
 | 
			
		||||
        'block_content',
 | 
			
		||||
      ],
 | 
			
		||||
      'Media' => [
 | 
			
		||||
        'media',
 | 
			
		||||
      ],
 | 
			
		||||
      'Test entity - revisions, data table, and published interface' => [
 | 
			
		||||
        'entity_test_mulrevpub',
 | 
			
		||||
      ],
 | 
			
		||||
      'Entity Test with revisions and published status' => [
 | 
			
		||||
        'entity_test_revpub',
 | 
			
		||||
      ],
 | 
			
		||||
    ];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function createEntity($entity_type_id, $moderation_state = 'published', $create_workflow = TRUE) {
 | 
			
		||||
    $entity = $this->workspaceManager->executeOutsideWorkspace(function () use ($entity_type_id, $moderation_state, $create_workflow) {
 | 
			
		||||
      return parent::createEntity($entity_type_id, $moderation_state, $create_workflow);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return $entity;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function createEditorialWorkflow() {
 | 
			
		||||
    $workflow = $this->workspaceManager->executeOutsideWorkspace(function () {
 | 
			
		||||
      return $this->traitCreateEditorialWorkflow();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return $workflow;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function addEntityTypeAndBundleToWorkflow(WorkflowInterface $workflow, $entity_type_id, $bundle): void {
 | 
			
		||||
    $this->workspaceManager->executeOutsideWorkspace(function () use ($workflow, $entity_type_id, $bundle) {
 | 
			
		||||
      $this->traitAddEntityTypeAndBundleToWorkflow($workflow, $entity_type_id, $bundle);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function createContentType(array $values = []) {
 | 
			
		||||
    $note_type = $this->workspaceManager->executeOutsideWorkspace(function () use ($values) {
 | 
			
		||||
      return $this->traitCreateContentType($values);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return $note_type;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function assertDefaultRevision(EntityInterface $entity, int $revision_id, $published = TRUE): void {
 | 
			
		||||
    // In the context of a workspace, the default revision ID is always the
 | 
			
		||||
    // latest workspace-specific revision, so we need to adjust the expectation
 | 
			
		||||
    // of the parent assertion.
 | 
			
		||||
    $revision_id = (int) $this->entityTypeManager->getStorage($entity->getEntityTypeId())->load($entity->id())->getRevisionId();
 | 
			
		||||
 | 
			
		||||
    // Additionally, the publishing status of the default revision is not
 | 
			
		||||
    // relevant in a workspace, because getting an entity to a "published"
 | 
			
		||||
    // moderation state doesn't automatically make it the default revision, so
 | 
			
		||||
    // we have to disable that assertion.
 | 
			
		||||
    $published = NULL;
 | 
			
		||||
 | 
			
		||||
    parent::assertDefaultRevision($entity, $revision_id, $published);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,114 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\content_moderation\Traits;
 | 
			
		||||
 | 
			
		||||
use Drupal\content_moderation\Plugin\WorkflowType\ContentModerationInterface;
 | 
			
		||||
use Drupal\workflows\Entity\Workflow;
 | 
			
		||||
use Drupal\workflows\WorkflowInterface;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Provides functionality for testing content moderation.
 | 
			
		||||
 */
 | 
			
		||||
trait ContentModerationTestTrait {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Creates the editorial workflow.
 | 
			
		||||
   *
 | 
			
		||||
   * @return \Drupal\workflows\Entity\Workflow
 | 
			
		||||
   *   The editorial workflow entity.
 | 
			
		||||
   */
 | 
			
		||||
  protected function createEditorialWorkflow() {
 | 
			
		||||
    // Allow this method to be called twice from the same test method.
 | 
			
		||||
    if ($workflow = Workflow::load('editorial')) {
 | 
			
		||||
      return $workflow;
 | 
			
		||||
    }
 | 
			
		||||
    $workflow = Workflow::create([
 | 
			
		||||
      'type' => 'content_moderation',
 | 
			
		||||
      'id' => 'editorial',
 | 
			
		||||
      'label' => 'Editorial',
 | 
			
		||||
      'type_settings' => [
 | 
			
		||||
        'states' => [
 | 
			
		||||
          'archived' => [
 | 
			
		||||
            'label' => 'Archived',
 | 
			
		||||
            'weight' => 5,
 | 
			
		||||
            'published' => FALSE,
 | 
			
		||||
            'default_revision' => TRUE,
 | 
			
		||||
          ],
 | 
			
		||||
          'draft' => [
 | 
			
		||||
            'label' => 'Draft',
 | 
			
		||||
            'published' => FALSE,
 | 
			
		||||
            'default_revision' => FALSE,
 | 
			
		||||
            'weight' => -5,
 | 
			
		||||
          ],
 | 
			
		||||
          'published' => [
 | 
			
		||||
            'label' => 'Published',
 | 
			
		||||
            'published' => TRUE,
 | 
			
		||||
            'default_revision' => TRUE,
 | 
			
		||||
            'weight' => 0,
 | 
			
		||||
          ],
 | 
			
		||||
        ],
 | 
			
		||||
        'transitions' => [
 | 
			
		||||
          'archive' => [
 | 
			
		||||
            'label' => 'Archive',
 | 
			
		||||
            'from' => ['published'],
 | 
			
		||||
            'to' => 'archived',
 | 
			
		||||
            'weight' => 2,
 | 
			
		||||
          ],
 | 
			
		||||
          'archived_draft' => [
 | 
			
		||||
            'label' => 'Restore to Draft',
 | 
			
		||||
            'from' => ['archived'],
 | 
			
		||||
            'to' => 'draft',
 | 
			
		||||
            'weight' => 3,
 | 
			
		||||
          ],
 | 
			
		||||
          'archived_published' => [
 | 
			
		||||
            'label' => 'Restore',
 | 
			
		||||
            'from' => ['archived'],
 | 
			
		||||
            'to' => 'published',
 | 
			
		||||
            'weight' => 4,
 | 
			
		||||
          ],
 | 
			
		||||
          'create_new_draft' => [
 | 
			
		||||
            'label' => 'Create New Draft',
 | 
			
		||||
            'to' => 'draft',
 | 
			
		||||
            'weight' => 0,
 | 
			
		||||
            'from' => [
 | 
			
		||||
              'draft',
 | 
			
		||||
              'published',
 | 
			
		||||
            ],
 | 
			
		||||
          ],
 | 
			
		||||
          'publish' => [
 | 
			
		||||
            'label' => 'Publish',
 | 
			
		||||
            'to' => 'published',
 | 
			
		||||
            'weight' => 1,
 | 
			
		||||
            'from' => [
 | 
			
		||||
              'draft',
 | 
			
		||||
              'published',
 | 
			
		||||
            ],
 | 
			
		||||
          ],
 | 
			
		||||
        ],
 | 
			
		||||
      ],
 | 
			
		||||
    ]);
 | 
			
		||||
    $workflow->save();
 | 
			
		||||
    return $workflow;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Adds an entity type ID / bundle ID to the given workflow.
 | 
			
		||||
   *
 | 
			
		||||
   * @param \Drupal\workflows\WorkflowInterface $workflow
 | 
			
		||||
   *   A workflow object.
 | 
			
		||||
   * @param string $entity_type_id
 | 
			
		||||
   *   The entity type ID to add.
 | 
			
		||||
   * @param string $bundle
 | 
			
		||||
   *   The bundle ID to add.
 | 
			
		||||
   */
 | 
			
		||||
  protected function addEntityTypeAndBundleToWorkflow(WorkflowInterface $workflow, $entity_type_id, $bundle) {
 | 
			
		||||
    $moderation = $workflow->getTypePlugin();
 | 
			
		||||
    if ($moderation instanceof ContentModerationInterface) {
 | 
			
		||||
      $moderation->addEntityTypeAndBundle($entity_type_id, $bundle);
 | 
			
		||||
      $workflow->save();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,252 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\content_moderation\Unit;
 | 
			
		||||
 | 
			
		||||
use Drupal\content_moderation\Routing\ContentModerationRouteSubscriber;
 | 
			
		||||
use Drupal\Core\Entity\EntityBase;
 | 
			
		||||
use Drupal\Core\Entity\EntityTypeInterface;
 | 
			
		||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
 | 
			
		||||
use Drupal\Core\Routing\RouteBuildEvent;
 | 
			
		||||
use Drupal\Tests\UnitTestCase;
 | 
			
		||||
use Symfony\Component\Routing\Route;
 | 
			
		||||
use Symfony\Component\Routing\RouteCollection;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @coversDefaultClass \Drupal\content_moderation\Routing\ContentModerationRouteSubscriber
 | 
			
		||||
 *
 | 
			
		||||
 * @group content_moderation
 | 
			
		||||
 */
 | 
			
		||||
class ContentModerationRouteSubscriberTest extends UnitTestCase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The test content moderation route subscriber.
 | 
			
		||||
   *
 | 
			
		||||
   * @var \Drupal\content_moderation\Routing\ContentModerationRouteSubscriber
 | 
			
		||||
   */
 | 
			
		||||
  protected $routeSubscriber;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
 | 
			
		||||
    /** @var \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager */
 | 
			
		||||
    $entity_type_manager = $this->createMock(EntityTypeManagerInterface::class);
 | 
			
		||||
    $this->routeSubscriber = new ContentModerationRouteSubscriber($entity_type_manager);
 | 
			
		||||
    $this->setupEntityTypes();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Creates the entity type manager mock returning entity type objects.
 | 
			
		||||
   */
 | 
			
		||||
  protected function setupEntityTypes(): void {
 | 
			
		||||
    $definition = $this->createMock(EntityTypeInterface::class);
 | 
			
		||||
    $definition->expects($this->any())
 | 
			
		||||
      ->method('getClass')
 | 
			
		||||
      ->willReturn(TestEntity::class);
 | 
			
		||||
    $definition->expects($this->any())
 | 
			
		||||
      ->method('isRevisionable')
 | 
			
		||||
      ->willReturn(FALSE);
 | 
			
		||||
    $revisionable_definition = $this->createMock(EntityTypeInterface::class);
 | 
			
		||||
    $revisionable_definition->expects($this->any())
 | 
			
		||||
      ->method('getClass')
 | 
			
		||||
      ->willReturn(TestEntity::class);
 | 
			
		||||
    $revisionable_definition->expects($this->any())
 | 
			
		||||
      ->method('isRevisionable')
 | 
			
		||||
      ->willReturn(TRUE);
 | 
			
		||||
    $entity_types = [
 | 
			
		||||
      'entity_test' => $definition,
 | 
			
		||||
      'entity_test_rev' => $revisionable_definition,
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    $reflector = new \ReflectionProperty($this->routeSubscriber, 'moderatedEntityTypes');
 | 
			
		||||
    $reflector->setValue($this->routeSubscriber, $entity_types);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Data provider for ::testSetLatestRevisionFlag.
 | 
			
		||||
   */
 | 
			
		||||
  public static function setLatestRevisionFlagTestCases() {
 | 
			
		||||
    return [
 | 
			
		||||
      'Entity parameter not on an entity form' => [
 | 
			
		||||
        [],
 | 
			
		||||
        [
 | 
			
		||||
          'entity_test' => [
 | 
			
		||||
            'type' => 'entity:entity_test_rev',
 | 
			
		||||
          ],
 | 
			
		||||
        ],
 | 
			
		||||
      ],
 | 
			
		||||
      'Entity parameter on an entity form' => [
 | 
			
		||||
        [
 | 
			
		||||
          '_entity_form' => 'entity_test_rev.edit',
 | 
			
		||||
        ],
 | 
			
		||||
        [
 | 
			
		||||
          'entity_test_rev' => [
 | 
			
		||||
            'type' => 'entity:entity_test_rev',
 | 
			
		||||
          ],
 | 
			
		||||
        ],
 | 
			
		||||
        [
 | 
			
		||||
          'entity_test_rev' => [
 | 
			
		||||
            'type' => 'entity:entity_test_rev',
 | 
			
		||||
            'load_latest_revision' => TRUE,
 | 
			
		||||
          ],
 | 
			
		||||
        ],
 | 
			
		||||
      ],
 | 
			
		||||
      'Entity form with no operation' => [
 | 
			
		||||
        [
 | 
			
		||||
          '_entity_form' => 'entity_test_rev',
 | 
			
		||||
        ],
 | 
			
		||||
        [
 | 
			
		||||
          'entity_test_rev' => [
 | 
			
		||||
            'type' => 'entity:entity_test_rev',
 | 
			
		||||
          ],
 | 
			
		||||
        ],
 | 
			
		||||
        [
 | 
			
		||||
          'entity_test_rev' => [
 | 
			
		||||
            'type' => 'entity:entity_test_rev',
 | 
			
		||||
            'load_latest_revision' => TRUE,
 | 
			
		||||
          ],
 | 
			
		||||
        ],
 | 
			
		||||
      ],
 | 
			
		||||
      'Non-moderated entity form' => [
 | 
			
		||||
        [
 | 
			
		||||
          '_entity_form' => 'entity_test_mulrev',
 | 
			
		||||
        ],
 | 
			
		||||
        [
 | 
			
		||||
          'entity_test_mulrev' => [
 | 
			
		||||
            'type' => 'entity:entity_test_mulrev',
 | 
			
		||||
          ],
 | 
			
		||||
        ],
 | 
			
		||||
      ],
 | 
			
		||||
      'Multiple entity parameters on an entity form' => [
 | 
			
		||||
        [
 | 
			
		||||
          '_entity_form' => 'entity_test_rev.edit',
 | 
			
		||||
        ],
 | 
			
		||||
        [
 | 
			
		||||
          'entity_test_rev' => [
 | 
			
		||||
            'type' => 'entity:entity_test_rev',
 | 
			
		||||
          ],
 | 
			
		||||
          'node' => [
 | 
			
		||||
            'type' => 'entity:node',
 | 
			
		||||
          ],
 | 
			
		||||
        ],
 | 
			
		||||
        [
 | 
			
		||||
          'entity_test_rev' => [
 | 
			
		||||
            'type' => 'entity:entity_test_rev',
 | 
			
		||||
            'load_latest_revision' => TRUE,
 | 
			
		||||
          ],
 | 
			
		||||
          'node' => [
 | 
			
		||||
            'type' => 'entity:node',
 | 
			
		||||
          ],
 | 
			
		||||
        ],
 | 
			
		||||
      ],
 | 
			
		||||
      'Overridden load_latest_revision flag does not change' => [
 | 
			
		||||
        [
 | 
			
		||||
          '_entity_form' => 'entity_test_rev.edit',
 | 
			
		||||
        ],
 | 
			
		||||
        [
 | 
			
		||||
          'entity_test_rev' => [
 | 
			
		||||
            'type' => 'entity:entity_test_rev',
 | 
			
		||||
            'load_latest_revision' => FALSE,
 | 
			
		||||
          ],
 | 
			
		||||
        ],
 | 
			
		||||
      ],
 | 
			
		||||
      'Non-revisionable entity type will not change' => [
 | 
			
		||||
        [
 | 
			
		||||
          '_entity_form' => 'entity_test.edit',
 | 
			
		||||
        ],
 | 
			
		||||
        [
 | 
			
		||||
          'entity_test' => [
 | 
			
		||||
            'type' => 'entity:entity_test',
 | 
			
		||||
          ],
 | 
			
		||||
        ],
 | 
			
		||||
        FALSE,
 | 
			
		||||
        FALSE,
 | 
			
		||||
      ],
 | 
			
		||||
      'Overridden load_latest_revision flag does not change with multiple parameters' => [
 | 
			
		||||
        [
 | 
			
		||||
          '_entity_form' => 'entity_test_rev.edit',
 | 
			
		||||
        ],
 | 
			
		||||
        [
 | 
			
		||||
          'entity_test_rev' => [
 | 
			
		||||
            'type' => 'entity:entity_test_rev',
 | 
			
		||||
          ],
 | 
			
		||||
          'node' => [
 | 
			
		||||
            'type' => 'entity:node',
 | 
			
		||||
            'load_latest_revision' => FALSE,
 | 
			
		||||
          ],
 | 
			
		||||
        ],
 | 
			
		||||
        [
 | 
			
		||||
          'entity_test_rev' => [
 | 
			
		||||
            'type' => 'entity:entity_test_rev',
 | 
			
		||||
            'load_latest_revision' => TRUE,
 | 
			
		||||
          ],
 | 
			
		||||
          'node' => [
 | 
			
		||||
            'type' => 'entity:node',
 | 
			
		||||
            'load_latest_revision' => FALSE,
 | 
			
		||||
          ],
 | 
			
		||||
        ],
 | 
			
		||||
      ],
 | 
			
		||||
      'Parameter without type is unchanged' => [
 | 
			
		||||
        [
 | 
			
		||||
          '_entity_form' => 'entity_test_rev.edit',
 | 
			
		||||
        ],
 | 
			
		||||
        [
 | 
			
		||||
          'entity_test_rev' => [
 | 
			
		||||
            'type' => 'entity:entity_test_rev',
 | 
			
		||||
          ],
 | 
			
		||||
          'unrelated_param' => [
 | 
			
		||||
            'foo' => 'bar',
 | 
			
		||||
          ],
 | 
			
		||||
        ],
 | 
			
		||||
        [
 | 
			
		||||
          'entity_test_rev' => [
 | 
			
		||||
            'type' => 'entity:entity_test_rev',
 | 
			
		||||
            'load_latest_revision' => TRUE,
 | 
			
		||||
          ],
 | 
			
		||||
          'unrelated_param' => [
 | 
			
		||||
            'foo' => 'bar',
 | 
			
		||||
          ],
 | 
			
		||||
        ],
 | 
			
		||||
      ],
 | 
			
		||||
    ];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests that the "load_latest_revision" flag is handled correctly.
 | 
			
		||||
   *
 | 
			
		||||
   * @param array $defaults
 | 
			
		||||
   *   The route defaults.
 | 
			
		||||
   * @param array $parameters
 | 
			
		||||
   *   The route parameters.
 | 
			
		||||
   * @param array|bool $expected_parameters
 | 
			
		||||
   *   (optional) The expected route parameters. Defaults to FALSE.
 | 
			
		||||
   *
 | 
			
		||||
   * @covers ::setLatestRevisionFlag
 | 
			
		||||
   *
 | 
			
		||||
   * @dataProvider setLatestRevisionFlagTestCases
 | 
			
		||||
   */
 | 
			
		||||
  public function testSetLatestRevisionFlag($defaults, $parameters, $expected_parameters = FALSE): void {
 | 
			
		||||
    $route = new Route('/foo/{entity_test}', $defaults, [], [
 | 
			
		||||
      'parameters' => $parameters,
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    $route_collection = new RouteCollection();
 | 
			
		||||
    $route_collection->add('test', $route);
 | 
			
		||||
    $event = new RouteBuildEvent($route_collection);
 | 
			
		||||
    $this->routeSubscriber->onAlterRoutes($event);
 | 
			
		||||
 | 
			
		||||
    // If expected parameters have not been provided, assert they are unchanged.
 | 
			
		||||
    $this->assertEquals($expected_parameters ?: $parameters, $route->getOption('parameters'));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A concrete entity.
 | 
			
		||||
 */
 | 
			
		||||
class TestEntity extends EntityBase {
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,75 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\content_moderation\Unit;
 | 
			
		||||
 | 
			
		||||
use Drupal\content_moderation\ContentPreprocess;
 | 
			
		||||
use Drupal\Core\Routing\CurrentRouteMatch;
 | 
			
		||||
use Drupal\node\Entity\Node;
 | 
			
		||||
use Drupal\Tests\UnitTestCase;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @coversDefaultClass \Drupal\content_moderation\ContentPreprocess
 | 
			
		||||
 *
 | 
			
		||||
 * @group content_moderation
 | 
			
		||||
 */
 | 
			
		||||
class ContentPreprocessTest extends UnitTestCase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @covers ::isLatestVersionPage
 | 
			
		||||
   * @dataProvider routeNodeProvider
 | 
			
		||||
   */
 | 
			
		||||
  public function testIsLatestVersionPage($route_name, $route_nid, $check_nid, $result, $message): void {
 | 
			
		||||
    $content_preprocess = new ContentPreprocess($this->setupCurrentRouteMatch($route_name, $route_nid));
 | 
			
		||||
    $node = $this->setupNode($check_nid);
 | 
			
		||||
    $this->assertEquals($result, $content_preprocess->isLatestVersionPage($node), $message);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Data provider for self::testIsLatestVersionPage().
 | 
			
		||||
   */
 | 
			
		||||
  public static function routeNodeProvider() {
 | 
			
		||||
    return [
 | 
			
		||||
      ['entity.node.canonical', 1, 1, FALSE, 'Not on the latest version tab route.'],
 | 
			
		||||
      ['entity.node.latest_version', 1, 1, TRUE, 'On the latest version tab route, with the route node.'],
 | 
			
		||||
      ['entity.node.latest_version', 1, 2, FALSE, 'On the latest version tab route, with a different node.'],
 | 
			
		||||
    ];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Mock the current route matching object.
 | 
			
		||||
   *
 | 
			
		||||
   * @param string $route_name
 | 
			
		||||
   *   The route to mock.
 | 
			
		||||
   * @param int $nid
 | 
			
		||||
   *   The node ID for mocking.
 | 
			
		||||
   *
 | 
			
		||||
   * @return \Drupal\Core\Routing\CurrentRouteMatch
 | 
			
		||||
   *   The mocked current route match object.
 | 
			
		||||
   */
 | 
			
		||||
  protected function setupCurrentRouteMatch($route_name, $nid) {
 | 
			
		||||
    $route_match = $this->prophesize(CurrentRouteMatch::class);
 | 
			
		||||
    $route_match->getRouteName()->willReturn($route_name);
 | 
			
		||||
    $route_match->getParameter('node')->willReturn($this->setupNode($nid));
 | 
			
		||||
 | 
			
		||||
    return $route_match->reveal();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Mock a node object.
 | 
			
		||||
   *
 | 
			
		||||
   * @param int $nid
 | 
			
		||||
   *   The node ID to mock.
 | 
			
		||||
   *
 | 
			
		||||
   * @return \Drupal\node\Entity\Node
 | 
			
		||||
   *   The mocked node.
 | 
			
		||||
   */
 | 
			
		||||
  protected function setupNode($nid) {
 | 
			
		||||
    $node = $this->prophesize(Node::class);
 | 
			
		||||
    $node->id()->willReturn($nid);
 | 
			
		||||
 | 
			
		||||
    return $node->reveal();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,205 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\content_moderation\Unit;
 | 
			
		||||
 | 
			
		||||
use Drupal\block_content\Entity\BlockContent;
 | 
			
		||||
use Drupal\Core\Access\AccessResultAllowed;
 | 
			
		||||
use Drupal\Core\Access\AccessResultForbidden;
 | 
			
		||||
use Drupal\Core\Access\AccessResultNeutral;
 | 
			
		||||
use Drupal\Core\Cache\Context\CacheContextsManager;
 | 
			
		||||
use Drupal\Core\Routing\RouteMatch;
 | 
			
		||||
use Drupal\Core\Session\AccountInterface;
 | 
			
		||||
use Drupal\node\Entity\Node;
 | 
			
		||||
use Drupal\content_moderation\Access\LatestRevisionCheck;
 | 
			
		||||
use Drupal\content_moderation\ModerationInformation;
 | 
			
		||||
use Drupal\Tests\UnitTestCase;
 | 
			
		||||
use Drupal\user\EntityOwnerInterface;
 | 
			
		||||
use Prophecy\Argument;
 | 
			
		||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
 | 
			
		||||
use Symfony\Component\Routing\Route;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @coversDefaultClass \Drupal\content_moderation\Access\LatestRevisionCheck
 | 
			
		||||
 * @group content_moderation
 | 
			
		||||
 */
 | 
			
		||||
class LatestRevisionCheckTest extends UnitTestCase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
 | 
			
		||||
    // Initialize Drupal container since the cache context manager is needed.
 | 
			
		||||
    $contexts_manager = $this->prophesize(CacheContextsManager::class);
 | 
			
		||||
    $contexts_manager->assertValidTokens(Argument::any())->willReturn(TRUE);
 | 
			
		||||
    $builder = new ContainerBuilder();
 | 
			
		||||
    $builder->set('cache_contexts_manager', $contexts_manager->reveal());
 | 
			
		||||
    \Drupal::setContainer($builder);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the access check of the LatestRevisionCheck service.
 | 
			
		||||
   *
 | 
			
		||||
   * @param string $entity_class
 | 
			
		||||
   *   The class of the entity to mock.
 | 
			
		||||
   * @param string $entity_type
 | 
			
		||||
   *   The machine name of the entity to mock.
 | 
			
		||||
   * @param bool $has_pending_revision
 | 
			
		||||
   *   Whether this entity should have a pending revision in the system.
 | 
			
		||||
   * @param array $account_permissions
 | 
			
		||||
   *   An array of permissions the account has.
 | 
			
		||||
   * @param bool $is_owner
 | 
			
		||||
   *   Indicates if the user should be the owner of the entity.
 | 
			
		||||
   * @param string $result_class
 | 
			
		||||
   *   The AccessResult class that should result. One of AccessResultAllowed,
 | 
			
		||||
   *   AccessResultForbidden, AccessResultNeutral.
 | 
			
		||||
   *
 | 
			
		||||
   * @dataProvider accessSituationProvider
 | 
			
		||||
   */
 | 
			
		||||
  public function testLatestAccessPermissions($entity_class, $entity_type, $has_pending_revision, array $account_permissions, $is_owner, $result_class): void {
 | 
			
		||||
 | 
			
		||||
    /** @var \Drupal\Core\Session\AccountInterface $account */
 | 
			
		||||
    $account = $this->prophesize(AccountInterface::class);
 | 
			
		||||
    $possible_permissions = [
 | 
			
		||||
      'view latest version',
 | 
			
		||||
      'view any unpublished content',
 | 
			
		||||
      'view own unpublished content',
 | 
			
		||||
    ];
 | 
			
		||||
    foreach ($possible_permissions as $permission) {
 | 
			
		||||
      $account->hasPermission($permission)->willReturn(in_array($permission, $account_permissions));
 | 
			
		||||
    }
 | 
			
		||||
    $account->id()->willReturn(42);
 | 
			
		||||
 | 
			
		||||
    /** @var \Drupal\Core\Entity\EntityInterface $entity */
 | 
			
		||||
    $entity = $this->prophesize($entity_class);
 | 
			
		||||
    $entity->getCacheContexts()->willReturn([]);
 | 
			
		||||
    $entity->getCacheTags()->willReturn([]);
 | 
			
		||||
    $entity->getCacheMaxAge()->willReturn(0);
 | 
			
		||||
    if (is_subclass_of($entity_class, EntityOwnerInterface::class)) {
 | 
			
		||||
      $entity->getOwnerId()->willReturn($is_owner ? 42 : 3);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** @var \Drupal\content_moderation\ModerationInformation $mod_info */
 | 
			
		||||
    $mod_info = $this->prophesize(ModerationInformation::class);
 | 
			
		||||
    $mod_info->hasPendingRevision($entity->reveal())->willReturn($has_pending_revision);
 | 
			
		||||
 | 
			
		||||
    $route = $this->prophesize(Route::class);
 | 
			
		||||
 | 
			
		||||
    $route->getOption('_content_moderation_entity_type')->willReturn($entity_type);
 | 
			
		||||
 | 
			
		||||
    $route_match = $this->prophesize(RouteMatch::class);
 | 
			
		||||
    $route_match->getParameter($entity_type)->willReturn($entity->reveal());
 | 
			
		||||
 | 
			
		||||
    $lrc = new LatestRevisionCheck($mod_info->reveal());
 | 
			
		||||
 | 
			
		||||
    /** @var \Drupal\Core\Access\AccessResult $result */
 | 
			
		||||
    $result = $lrc->access($route->reveal(), $route_match->reveal(), $account->reveal());
 | 
			
		||||
 | 
			
		||||
    $this->assertInstanceOf($result_class, $result);
 | 
			
		||||
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Data provider for testLastAccessPermissions().
 | 
			
		||||
   */
 | 
			
		||||
  public static function accessSituationProvider() {
 | 
			
		||||
    return [
 | 
			
		||||
      // Node with global permissions and latest version.
 | 
			
		||||
      [
 | 
			
		||||
        Node::class,
 | 
			
		||||
        'node',
 | 
			
		||||
        TRUE,
 | 
			
		||||
        ['view latest version', 'view any unpublished content'],
 | 
			
		||||
        FALSE,
 | 
			
		||||
        AccessResultAllowed::class,
 | 
			
		||||
      ],
 | 
			
		||||
      // Node with global permissions and no latest version.
 | 
			
		||||
      [
 | 
			
		||||
        Node::class,
 | 
			
		||||
        'node',
 | 
			
		||||
        FALSE,
 | 
			
		||||
        ['view latest version', 'view any unpublished content'],
 | 
			
		||||
        FALSE,
 | 
			
		||||
        AccessResultForbidden::class,
 | 
			
		||||
      ],
 | 
			
		||||
      // Node with own content permissions and latest version.
 | 
			
		||||
      [
 | 
			
		||||
        Node::class,
 | 
			
		||||
        'node',
 | 
			
		||||
        TRUE,
 | 
			
		||||
        ['view latest version', 'view own unpublished content'],
 | 
			
		||||
        TRUE,
 | 
			
		||||
        AccessResultAllowed::class,
 | 
			
		||||
      ],
 | 
			
		||||
      // Node with own content permissions and no latest version.
 | 
			
		||||
      [
 | 
			
		||||
        Node::class,
 | 
			
		||||
        'node',
 | 
			
		||||
        FALSE,
 | 
			
		||||
        ['view latest version', 'view own unpublished content'],
 | 
			
		||||
        FALSE,
 | 
			
		||||
        AccessResultForbidden::class,
 | 
			
		||||
      ],
 | 
			
		||||
      // Node with own content permissions and latest version, but no perms to
 | 
			
		||||
      // view latest version.
 | 
			
		||||
      [
 | 
			
		||||
        Node::class,
 | 
			
		||||
        'node',
 | 
			
		||||
        TRUE,
 | 
			
		||||
        ['view own unpublished content'],
 | 
			
		||||
        TRUE,
 | 
			
		||||
        AccessResultNeutral::class,
 | 
			
		||||
      ],
 | 
			
		||||
      // Node with own content permissions and no latest version, but no perms
 | 
			
		||||
      // to view latest version.
 | 
			
		||||
      [
 | 
			
		||||
        Node::class,
 | 
			
		||||
        'node',
 | 
			
		||||
        TRUE,
 | 
			
		||||
        ['view own unpublished content'],
 | 
			
		||||
        FALSE,
 | 
			
		||||
        AccessResultNeutral::class,
 | 
			
		||||
      ],
 | 
			
		||||
      // Block with pending revision, and permissions to view any.
 | 
			
		||||
      [
 | 
			
		||||
        BlockContent::class,
 | 
			
		||||
        'block_content',
 | 
			
		||||
        TRUE,
 | 
			
		||||
        ['view latest version', 'view any unpublished content'],
 | 
			
		||||
        FALSE,
 | 
			
		||||
        AccessResultAllowed::class,
 | 
			
		||||
      ],
 | 
			
		||||
      // Block with no pending revision.
 | 
			
		||||
      [
 | 
			
		||||
        BlockContent::class,
 | 
			
		||||
        'block_content',
 | 
			
		||||
        FALSE,
 | 
			
		||||
        ['view latest version', 'view any unpublished content'],
 | 
			
		||||
        FALSE,
 | 
			
		||||
        AccessResultForbidden::class,
 | 
			
		||||
      ],
 | 
			
		||||
      // Block with pending revision, but no permission to view any.
 | 
			
		||||
      [
 | 
			
		||||
        BlockContent::class,
 | 
			
		||||
        'block_content',
 | 
			
		||||
        TRUE,
 | 
			
		||||
        ['view latest version', 'view own unpublished content'],
 | 
			
		||||
        FALSE,
 | 
			
		||||
        AccessResultNeutral::class,
 | 
			
		||||
      ],
 | 
			
		||||
      // Block with no pending revision.
 | 
			
		||||
      [
 | 
			
		||||
        BlockContent::class,
 | 
			
		||||
        'block_content',
 | 
			
		||||
        FALSE,
 | 
			
		||||
        ['view latest version', 'view own unpublished content'],
 | 
			
		||||
        FALSE,
 | 
			
		||||
        AccessResultForbidden::class,
 | 
			
		||||
      ],
 | 
			
		||||
    ];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,156 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\content_moderation\Unit;
 | 
			
		||||
 | 
			
		||||
use Drupal\content_moderation\Entity\Handler\ModerationHandler;
 | 
			
		||||
use Drupal\Core\Entity\ContentEntityInterface;
 | 
			
		||||
use Drupal\Core\Entity\ContentEntityType;
 | 
			
		||||
use Drupal\Core\Entity\EntityStorageInterface;
 | 
			
		||||
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
 | 
			
		||||
use Drupal\Core\Entity\EntityTypeInterface;
 | 
			
		||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
 | 
			
		||||
use Drupal\Core\Session\AccountInterface;
 | 
			
		||||
use Drupal\content_moderation\ModerationInformation;
 | 
			
		||||
use Drupal\Tests\UnitTestCase;
 | 
			
		||||
use Drupal\workflows\WorkflowInterface;
 | 
			
		||||
use Prophecy\Argument;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @coversDefaultClass \Drupal\content_moderation\ModerationInformation
 | 
			
		||||
 * @group content_moderation
 | 
			
		||||
 */
 | 
			
		||||
class ModerationInformationTest extends UnitTestCase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Builds a mock user.
 | 
			
		||||
   *
 | 
			
		||||
   * @return \Drupal\Core\Session\AccountInterface
 | 
			
		||||
   *   The mocked user.
 | 
			
		||||
   */
 | 
			
		||||
  protected function getUser() {
 | 
			
		||||
    return $this->prophesize(AccountInterface::class)->reveal();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Returns a mock Entity Type Manager.
 | 
			
		||||
   *
 | 
			
		||||
   * @return \Drupal\Core\Entity\EntityTypeManagerInterface
 | 
			
		||||
   *   The mocked entity type manager.
 | 
			
		||||
   */
 | 
			
		||||
  protected function getEntityTypeManager() {
 | 
			
		||||
    $entity_type_manager = $this->prophesize(EntityTypeManagerInterface::class);
 | 
			
		||||
    $entity_type_manager->getHandler(Argument::any(), 'moderation')->willReturn(new ModerationHandler());
 | 
			
		||||
    return $entity_type_manager->reveal();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Sets up content moderation and entity type bundle info mocking.
 | 
			
		||||
   *
 | 
			
		||||
   * @param string $bundle
 | 
			
		||||
   *   The bundle ID.
 | 
			
		||||
   * @param string|null $workflow
 | 
			
		||||
   *   The workflow ID. If nul no workflow information is added to the bundle.
 | 
			
		||||
   *
 | 
			
		||||
   * @return \Drupal\Core\Entity\EntityTypeManagerInterface
 | 
			
		||||
   *   The mocked entity type manager.
 | 
			
		||||
   */
 | 
			
		||||
  public function setupModerationBundleInfo($bundle, $workflow = NULL) {
 | 
			
		||||
    $bundle_info_array = [];
 | 
			
		||||
    if ($workflow) {
 | 
			
		||||
      $bundle_info_array['workflow'] = $workflow;
 | 
			
		||||
    }
 | 
			
		||||
    $bundle_info = $this->prophesize(EntityTypeBundleInfoInterface::class);
 | 
			
		||||
    $bundle_info->getBundleInfo("test_entity_type")->willReturn([$bundle => $bundle_info_array]);
 | 
			
		||||
    $bundle_info->getBundleInfo("unmoderated_test_type")->willReturn([$bundle => []]);
 | 
			
		||||
 | 
			
		||||
    return $bundle_info->reveal();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @covers ::isModeratedEntityType
 | 
			
		||||
   */
 | 
			
		||||
  public function testIsModeratedEntityType(): void {
 | 
			
		||||
    $moderation_information = new ModerationInformation($this->getEntityTypeManager(), $this->setupModerationBundleInfo('test_bundle', 'workflow'));
 | 
			
		||||
 | 
			
		||||
    $moderated_entity_type = $this->prophesize(EntityTypeInterface::class);
 | 
			
		||||
    $moderated_entity_type->id()->willReturn('test_entity_type');
 | 
			
		||||
 | 
			
		||||
    $unmoderated_entity_type = $this->prophesize(EntityTypeInterface::class);
 | 
			
		||||
    $unmoderated_entity_type->id()->willReturn('unmoderated_test_type');
 | 
			
		||||
 | 
			
		||||
    $this->assertTrue($moderation_information->isModeratedEntityType($moderated_entity_type->reveal()));
 | 
			
		||||
    $this->assertFalse($moderation_information->isModeratedEntityType($unmoderated_entity_type->reveal()));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @covers ::isModeratedEntity
 | 
			
		||||
   * @dataProvider providerWorkflow
 | 
			
		||||
   */
 | 
			
		||||
  public function testIsModeratedEntity($workflow, $expected): void {
 | 
			
		||||
    $moderation_information = new ModerationInformation($this->getEntityTypeManager(), $this->setupModerationBundleInfo('test_bundle', $workflow));
 | 
			
		||||
 | 
			
		||||
    $entity_type = new ContentEntityType([
 | 
			
		||||
      'id' => 'test_entity_type',
 | 
			
		||||
      'bundle_entity_type' => 'entity_test_bundle',
 | 
			
		||||
      'handlers' => ['moderation' => ModerationHandler::class],
 | 
			
		||||
    ]);
 | 
			
		||||
    $entity = $this->prophesize(ContentEntityInterface::class);
 | 
			
		||||
    $entity->getEntityType()->willReturn($entity_type);
 | 
			
		||||
    $entity->getEntityTypeId()->willReturn($entity_type->id());
 | 
			
		||||
    $entity->bundle()->willReturn('test_bundle');
 | 
			
		||||
 | 
			
		||||
    $this->assertEquals($expected, $moderation_information->isModeratedEntity($entity->reveal()));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @covers ::getWorkflowForEntity
 | 
			
		||||
   * @dataProvider providerWorkflow
 | 
			
		||||
   */
 | 
			
		||||
  public function testGetWorkflowForEntity($workflow): void {
 | 
			
		||||
    $entity_type_manager = $this->prophesize(EntityTypeManagerInterface::class);
 | 
			
		||||
    if ($workflow) {
 | 
			
		||||
      $workflow_entity = $this->prophesize(WorkflowInterface::class)->reveal();
 | 
			
		||||
      $workflow_storage = $this->prophesize(EntityStorageInterface::class);
 | 
			
		||||
      $workflow_storage->load('workflow')->willReturn($workflow_entity)->shouldBeCalled();
 | 
			
		||||
      $entity_type_manager->getStorage('workflow')->willReturn($workflow_storage->reveal());
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
      $workflow_entity = NULL;
 | 
			
		||||
    }
 | 
			
		||||
    $moderation_information = new ModerationInformation($entity_type_manager->reveal(), $this->setupModerationBundleInfo('test_bundle', $workflow));
 | 
			
		||||
    $entity = $this->prophesize(ContentEntityInterface::class);
 | 
			
		||||
    $entity->getEntityTypeId()->willReturn('test_entity_type');
 | 
			
		||||
    $entity->bundle()->willReturn('test_bundle');
 | 
			
		||||
 | 
			
		||||
    $this->assertEquals($workflow_entity, $moderation_information->getWorkflowForEntity($entity->reveal()));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @covers ::shouldModerateEntitiesOfBundle
 | 
			
		||||
   * @dataProvider providerWorkflow
 | 
			
		||||
   */
 | 
			
		||||
  public function testShouldModerateEntities($workflow, $expected): void {
 | 
			
		||||
    $entity_type = new ContentEntityType([
 | 
			
		||||
      'id' => 'test_entity_type',
 | 
			
		||||
      'bundle_entity_type' => 'entity_test_bundle',
 | 
			
		||||
      'handlers' => ['moderation' => ModerationHandler::class],
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    $moderation_information = new ModerationInformation($this->getEntityTypeManager(), $this->setupModerationBundleInfo('test_bundle', $workflow));
 | 
			
		||||
 | 
			
		||||
    $this->assertEquals($expected, $moderation_information->shouldModerateEntitiesOfBundle($entity_type, 'test_bundle'));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Data provider for several tests.
 | 
			
		||||
   */
 | 
			
		||||
  public static function providerWorkflow() {
 | 
			
		||||
    return [
 | 
			
		||||
      [NULL, FALSE],
 | 
			
		||||
      ['workflow', TRUE],
 | 
			
		||||
    ];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,114 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\content_moderation\Unit;
 | 
			
		||||
 | 
			
		||||
use Drupal\content_moderation\ModerationInformationInterface;
 | 
			
		||||
use Drupal\Core\DependencyInjection\ContainerBuilder;
 | 
			
		||||
use Drupal\Core\Entity\ContentEntityInterface;
 | 
			
		||||
use Drupal\Core\Session\AccountInterface;
 | 
			
		||||
use Drupal\content_moderation\StateTransitionValidation;
 | 
			
		||||
use Drupal\Tests\UnitTestCase;
 | 
			
		||||
use Drupal\workflow_type_test\Plugin\WorkflowType\TestType;
 | 
			
		||||
use Drupal\workflows\Entity\Workflow;
 | 
			
		||||
use Drupal\workflows\WorkflowTypeManager;
 | 
			
		||||
use Prophecy\Argument;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @coversDefaultClass \Drupal\content_moderation\StateTransitionValidation
 | 
			
		||||
 * @group content_moderation
 | 
			
		||||
 */
 | 
			
		||||
class StateTransitionValidationTest extends UnitTestCase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * A test workflow.
 | 
			
		||||
   *
 | 
			
		||||
   * @var \Drupal\workflows\WorkflowInterface
 | 
			
		||||
   */
 | 
			
		||||
  protected $workflow;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
 | 
			
		||||
    // Create a container so that the plugin manager and workflow type can be
 | 
			
		||||
    // mocked.
 | 
			
		||||
    $container = new ContainerBuilder();
 | 
			
		||||
    $workflow_manager = $this->prophesize(WorkflowTypeManager::class);
 | 
			
		||||
    $workflow_manager->createInstance('content_moderation', Argument::any())->willReturn(new TestType([], '', []));
 | 
			
		||||
    $container->set('plugin.manager.workflows.type', $workflow_manager->reveal());
 | 
			
		||||
    \Drupal::setContainer($container);
 | 
			
		||||
 | 
			
		||||
    $this->workflow = new Workflow(['id' => 'process', 'type' => 'content_moderation'], 'workflow');
 | 
			
		||||
    $this->workflow
 | 
			
		||||
      ->getTypePlugin()
 | 
			
		||||
      ->addState('draft', 'draft')
 | 
			
		||||
      ->addState('needs_review', 'needs_review')
 | 
			
		||||
      ->addState('published', 'published')
 | 
			
		||||
      ->addTransition('draft', 'draft', ['draft'], 'draft')
 | 
			
		||||
      ->addTransition('review', 'review', ['draft'], 'needs_review')
 | 
			
		||||
      ->addTransition('publish', 'publish', ['needs_review', 'published'], 'published');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Verifies user-aware transition validation.
 | 
			
		||||
   *
 | 
			
		||||
   * @param string $from_id
 | 
			
		||||
   *   The state to transition from.
 | 
			
		||||
   * @param string $to_id
 | 
			
		||||
   *   The state to transition to.
 | 
			
		||||
   * @param string $permission
 | 
			
		||||
   *   The permission to give the user, or not.
 | 
			
		||||
   * @param bool $allowed
 | 
			
		||||
   *   Whether or not to grant a user this permission.
 | 
			
		||||
   * @param bool $result
 | 
			
		||||
   *   Whether getValidTransitions() is expected to have the.
 | 
			
		||||
   *
 | 
			
		||||
   * @dataProvider userTransitionsProvider
 | 
			
		||||
   */
 | 
			
		||||
  public function testUserSensitiveValidTransitions($from_id, $to_id, $permission, $allowed, $result): void {
 | 
			
		||||
    $user = $this->prophesize(AccountInterface::class);
 | 
			
		||||
    // The one listed permission will be returned as instructed; Any others are
 | 
			
		||||
    // always denied.
 | 
			
		||||
    $user->hasPermission($permission)->willReturn($allowed);
 | 
			
		||||
    $user->hasPermission(Argument::type('string'))->willReturn(FALSE);
 | 
			
		||||
 | 
			
		||||
    $entity = $this->prophesize(ContentEntityInterface::class);
 | 
			
		||||
    $entity = $entity->reveal();
 | 
			
		||||
    $entity->moderation_state = new \stdClass();
 | 
			
		||||
    $entity->moderation_state->value = $from_id;
 | 
			
		||||
 | 
			
		||||
    $moderation_info = $this->prophesize(ModerationInformationInterface::class);
 | 
			
		||||
    $moderation_info->getWorkflowForEntity($entity)->willReturn($this->workflow);
 | 
			
		||||
 | 
			
		||||
    $validator = new StateTransitionValidation($moderation_info->reveal());
 | 
			
		||||
    $has_transition = FALSE;
 | 
			
		||||
    foreach ($validator->getValidTransitions($entity, $user->reveal()) as $transition) {
 | 
			
		||||
      if ($transition->to()->id() === $to_id) {
 | 
			
		||||
        $has_transition = TRUE;
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    $this->assertSame($result, $has_transition);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Data provider for the user transition test.
 | 
			
		||||
   */
 | 
			
		||||
  public static function userTransitionsProvider() {
 | 
			
		||||
    // The user has the right permission, so let it through.
 | 
			
		||||
    $ret[] = ['draft', 'draft', 'use process transition draft', TRUE, TRUE];
 | 
			
		||||
 | 
			
		||||
    // The user doesn't have the right permission, block it.
 | 
			
		||||
    $ret[] = ['draft', 'draft', 'use process transition draft', FALSE, FALSE];
 | 
			
		||||
 | 
			
		||||
    // The user has some other permission that doesn't matter.
 | 
			
		||||
    $ret[] = ['draft', 'draft', 'use process transition review', TRUE, FALSE];
 | 
			
		||||
 | 
			
		||||
    return $ret;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user