first commit

This commit is contained in:
2024-07-15 12:33:27 +02:00
commit ce50ae282b
22084 changed files with 2623791 additions and 0 deletions

View File

@@ -0,0 +1,12 @@
name: 'Workflow Third Party Settings Test'
type: module
description: 'Allows third party settings on workflows to be tested.'
package: Testing
# version: VERSION
dependencies:
- drupal:workflows
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,38 @@
<?php
namespace Drupal\workflow_type_test\Form;
use Drupal\Core\Form\FormStateInterface;
use Drupal\workflows\Plugin\WorkflowTypeConfigureFormBase;
/**
* Form to configure the complex test workflow type.
*
* @see \Drupal\workflow_type_test\Plugin\WorkflowType\ComplexTestType
*/
class ComplexTestTypeConfigureForm extends WorkflowTypeConfigureFormBase {
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$type_configuration = $this->workflowType->getConfiguration();
$form['example_setting'] = [
'#type' => 'textfield',
'#title' => $this->t('Example global workflow setting'),
'#description' => $this->t('Extra information added to the workflow'),
'#default_value' => $type_configuration['example_setting'],
];
return $form;
}
/**
* {@inheritdoc}
*/
public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
$type_configuration = $this->workflowType->getConfiguration();
$type_configuration['example_setting'] = $form_state->getValue('example_setting');
$this->workflowType->setConfiguration($type_configuration);
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace Drupal\workflow_type_test\Form;
use Drupal\Core\Form\FormStateInterface;
use Drupal\workflows\Plugin\WorkflowTypeStateFormBase;
/**
* Form to configure the complex test workflow states.
*
* @see \Drupal\workflow_type_test\Plugin\WorkflowType\ComplexTestType
*/
class ComplexTestTypeStateForm extends WorkflowTypeStateFormBase {
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$state = $form_state->get('state');
$configuration = $this->workflowType->getConfiguration();
$form['extra'] = [
'#type' => 'textfield',
'#title' => $this->t('Extra'),
'#description' => $this->t('Extra information added to state'),
'#default_value' => $state && isset($configuration['states'][$state->id()]['extra']) ? $configuration['states'][$state->id()]['extra'] : '',
];
return $form;
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace Drupal\workflow_type_test\Form;
use Drupal\Core\Form\FormStateInterface;
use Drupal\workflows\Plugin\WorkflowTypeTransitionFormBase;
/**
* Form to configure the complex test workflow states.
*
* @see \Drupal\workflow_type_test\Plugin\WorkflowType\ComplexTestType
*/
class ComplexTestTypeTransitionForm extends WorkflowTypeTransitionFormBase {
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$transition = $form_state->get('transition');
$configuration = $this->workflowType->getConfiguration();
$form['extra'] = [
'#type' => 'textfield',
'#title' => $this->t('Extra'),
'#description' => $this->t('Extra information added to transition'),
'#default_value' => $transition && isset($configuration['transitions'][$transition->id()]['extra']) ? $configuration['transitions'][$transition->id()]['extra'] : '',
];
return $form;
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace Drupal\workflow_type_test\Plugin\WorkflowType;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\workflows\Attribute\WorkflowType;
use Drupal\workflows\Plugin\WorkflowTypeBase;
/**
* Test workflow type.
*/
#[WorkflowType(
id: 'workflow_type_complex_test',
label: new TranslatableMarkup('Workflow Type Complex Test'),
forms: [
'configure' => '\Drupal\workflow_type_test\Form\ComplexTestTypeConfigureForm',
'state' => '\Drupal\workflow_type_test\Form\ComplexTestTypeStateForm',
'transition' => '\Drupal\workflow_type_test\Form\ComplexTestTypeTransitionForm',
]
)]
class ComplexTestType extends WorkflowTypeBase {
use StringTranslationTrait;
/**
* {@inheritdoc}
*/
public function onDependencyRemoval(array $dependencies) {
// Always return TRUE to allow the logic in
// \Drupal\workflows\Entity\Workflow::onDependencyRemoval() to be tested.
return TRUE;
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return parent::defaultConfiguration() + [
'example_setting' => '',
];
}
}

View File

@@ -0,0 +1,99 @@
<?php
namespace Drupal\workflow_type_test\Plugin\WorkflowType;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\workflows\Attribute\WorkflowType;
use Drupal\workflows\Plugin\WorkflowTypeBase;
use Drupal\workflows\State;
/**
* Test workflow type.
*/
#[WorkflowType(
id: 'predefined_states_workflow_test_type',
label: new TranslatableMarkup('Predefined States Workflow Test Type'),
required_states: [
'pay_blinds',
'bet',
'raise',
'fold',
]
)]
class PredefinedStatesWorkflowTestType extends WorkflowTypeBase {
/**
* {@inheritdoc}
*/
public function getStates($state_ids = NULL) {
return array_filter([
'pay_blinds' => new State($this, 'pay_blinds', 'Pay Blinds'),
'bet' => new State($this, 'bet', 'Bet'),
'raise' => new State($this, 'raise', 'Raise'),
'fold' => new State($this, 'fold', 'Fold'),
], function ($state) use ($state_ids) {
return is_array($state_ids) ? in_array($state->id(), $state_ids) : TRUE;
});
}
/**
* {@inheritdoc}
*/
public function getState($state_id) {
$states = $this->getStates();
if (!isset($states[$state_id])) {
throw new \InvalidArgumentException("The state '$state_id' does not exist in workflow.'");
}
return $states[$state_id];
}
/**
* {@inheritdoc}
*/
public function hasState($state_id) {
$states = $this->getStates();
return isset($states[$state_id]);
}
/**
* {@inheritdoc}
*/
public function addState($state_id, $label) {
// States cannot be added on this workflow.
return $this;
}
/**
* {@inheritdoc}
*/
public function setStateLabel($state_id, $label) {
// States cannot be altered on this workflow.
return $this;
}
/**
* {@inheritdoc}
*/
public function setStateWeight($state_id, $weight) {
// States cannot be altered on this workflow.
return $this;
}
/**
* {@inheritdoc}
*/
public function deleteState($state_id) {
// States cannot be deleted on this workflow.
return $this;
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return [
'transitions' => [],
];
}
}

View File

@@ -0,0 +1,53 @@
<?php
namespace Drupal\workflow_type_test\Plugin\WorkflowType;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\workflows\Attribute\WorkflowType;
use Drupal\workflows\Plugin\WorkflowTypeBase;
/**
* Test workflow type.
*/
#[WorkflowType(
id: 'workflow_type_required_state_test',
label: new TranslatableMarkup('Required State Type Test'),
required_states: [
'fresh',
'rotten',
]
)]
class RequiredStateTestType extends WorkflowTypeBase {
use StringTranslationTrait;
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return [
'states' => [
'fresh' => [
'label' => 'Fresh',
'weight' => 0,
],
'rotten' => [
'label' => 'Rotten',
'weight' => 1,
],
],
'transitions' => [
'rot' => [
'label' => 'Rot',
'to' => 'rotten',
'weight' => 0,
'from' => [
'fresh',
],
],
],
];
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace Drupal\workflow_type_test\Plugin\WorkflowType;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\workflows\Attribute\WorkflowType;
use Drupal\workflows\Plugin\WorkflowTypeBase;
/**
* Test workflow type.
*/
#[WorkflowType(
id: 'workflow_type_test',
label: new TranslatableMarkup('Workflow Type Test')
)]
class TestType extends WorkflowTypeBase {
/**
* {@inheritdoc}
*/
public function getRequiredStates() {
// Normally this is obtained from the annotation but we get from state to
// allow dynamic testing.
return \Drupal::state()->get('workflow_type_test.required_states', []);
}
}

View File

@@ -0,0 +1,69 @@
<?php
namespace Drupal\workflow_type_test\Plugin\WorkflowType;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\workflows\Attribute\WorkflowType;
use Drupal\workflows\Plugin\WorkflowTypeBase;
use Drupal\workflows\WorkflowInterface;
/**
* A test workflow with custom state/transition access rules applied.
*/
#[WorkflowType(
id: 'workflow_custom_access_type',
label: new TranslatableMarkup('Workflow Custom Access Type Test')
)]
class WorkflowCustomAccessType extends WorkflowTypeBase {
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return [
'states' => [
'cannot_update' => [
'label' => 'Cannot Update State',
'weight' => 0,
],
'cannot_delete' => [
'label' => 'Cannot Delete State',
'weight' => 0,
],
],
'transitions' => [
'cannot_update' => [
'label' => 'Cannot Update Transition',
'to' => 'cannot_update',
'weight' => 0,
'from' => [
'cannot_update',
],
],
'cannot_delete' => [
'label' => 'Cannot Delete Transition',
'to' => 'cannot_delete',
'weight' => 1,
'from' => [
'cannot_delete',
],
],
],
];
}
/**
* Implements hook_ENTITY_TYPE_access().
*
* @see workflow_type_test_workflow_access
*/
public static function workflowAccess(WorkflowInterface $entity, $operation, AccountInterface $account) {
$forbidden_operations = \Drupal::state()->get('workflow_type_test_forbidden_operations', []);
return in_array($operation, $forbidden_operations, TRUE)
? AccessResult::forbidden()
: AccessResult::neutral();
}
}

View File

@@ -0,0 +1,12 @@
name: 'Workflow Type Test'
type: module
description: 'Provides a workflow type plugin for testing.'
package: Testing
# version: VERSION
dependencies:
- drupal:workflows
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,43 @@
<?php
/**
* @file
* Module file for workflow_type_test.
*/
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Session\AccountInterface;
use Drupal\workflow_type_test\Plugin\WorkflowType\WorkflowCustomAccessType;
use Drupal\workflows\WorkflowInterface;
/**
* Implements hook_workflow_type_info_alter().
*/
function workflow_type_test_workflow_type_info_alter(&$definitions) {
// Allow tests to override the workflow type definitions.
$state = \Drupal::state();
if ($state->get('workflow_type_test.plugin_definitions') !== NULL) {
$definitions = $state->get('workflow_type_test.plugin_definitions');
}
}
/**
* Sets the type plugin definitions override and clear the cache.
*
* @param array $definitions
* Definitions to set.
*/
function workflow_type_test_set_definitions($definitions) {
\Drupal::state()->set('workflow_type_test.plugin_definitions', $definitions);
\Drupal::service('plugin.manager.workflows.type')->clearCachedDefinitions();
}
/**
* Implements hook_ENTITY_TYPE_access() for the Workflow entity type.
*/
function workflow_type_test_workflow_access(WorkflowInterface $entity, $operation, AccountInterface $account) {
if ($entity->getTypePlugin()->getPluginId() === 'workflow_custom_access_type') {
return WorkflowCustomAccessType::workflowAccess($entity, $operation, $account);
}
return AccessResult::neutral();
}

View File

@@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\workflows\Functional;
use Drupal\Tests\system\Functional\Module\GenericModuleTestBase;
/**
* Generic module test for workflows.
*
* @group workflows
*/
class GenericTest extends GenericModuleTestBase {}

View File

@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\workflows\Functional\Rest;
use Drupal\Tests\rest\Functional\AnonResourceTestTrait;
/**
* @group rest
*/
class WorkflowJsonAnonTest extends WorkflowResourceTestBase {
use AnonResourceTestTrait;
/**
* {@inheritdoc}
*/
protected static $format = 'json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/json';
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
}

View File

@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\workflows\Functional\Rest;
use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
/**
* @group rest
*/
class WorkflowJsonBasicAuthTest extends WorkflowResourceTestBase {
use BasicAuthResourceTestTrait;
/**
* {@inheritdoc}
*/
protected static $modules = ['basic_auth'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected static $format = 'json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/json';
/**
* {@inheritdoc}
*/
protected static $auth = 'basic_auth';
}

View File

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\workflows\Functional\Rest;
use Drupal\Tests\rest\Functional\CookieResourceTestTrait;
/**
* @group rest
*/
class WorkflowJsonCookieTest extends WorkflowResourceTestBase {
use CookieResourceTestTrait;
/**
* {@inheritdoc}
*/
protected static $format = 'json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/json';
/**
* {@inheritdoc}
*/
protected static $auth = 'cookie';
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
}

View File

@@ -0,0 +1,110 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\workflows\Functional\Rest;
use Drupal\Tests\rest\Functional\EntityResource\ConfigEntityResourceTestBase;
use Drupal\workflows\Entity\Workflow;
/**
* ResourceTestBase for Workflow entity.
*/
abstract class WorkflowResourceTestBase extends ConfigEntityResourceTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'workflows',
'workflow_type_test',
];
/**
* {@inheritdoc}
*/
protected static $entityTypeId = 'workflow';
/**
* {@inheritdoc}
*/
protected static $patchProtectedFieldNames = [];
/**
* The Workflow entity.
*
* @var \Drupal\workflows\WorkflowInterface
*/
protected $entity;
/**
* {@inheritdoc}
*/
protected function setUpAuthorization($method) {
$this->grantPermissionsToTestedRole(['administer workflows']);
}
/**
* {@inheritdoc}
*/
protected function createEntity() {
$workflow = Workflow::create([
'id' => 'rest_workflow',
'label' => 'REST Workflow',
'type' => 'workflow_type_complex_test',
]);
$workflow
->getTypePlugin()
->addState('draft', 'Draft')
->addState('published', 'Published');
$configuration = $workflow->getTypePlugin()->getConfiguration();
$configuration['example_setting'] = 'foo';
$configuration['states']['draft']['extra'] = 'bar';
$workflow->getTypePlugin()->setConfiguration($configuration);
$workflow->save();
return $workflow;
}
/**
* {@inheritdoc}
*/
protected function getExpectedNormalizedEntity() {
return [
'dependencies' => [
'module' => [
'workflow_type_test',
],
],
'id' => 'rest_workflow',
'label' => 'REST Workflow',
'langcode' => 'en',
'status' => TRUE,
'type' => 'workflow_type_complex_test',
'type_settings' => [
'states' => [
'draft' => [
'extra' => 'bar',
'label' => 'Draft',
'weight' => 0,
],
'published' => [
'label' => 'Published',
'weight' => 1,
],
],
'transitions' => [],
'example_setting' => 'foo',
],
'uuid' => $this->entity->uuid(),
];
}
/**
* {@inheritdoc}
*/
protected function getNormalizedPostEntity() {
// @todo Update in https://www.drupal.org/node/2300677.
return [];
}
}

View File

@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\workflows\Functional\Rest;
use Drupal\Tests\rest\Functional\AnonResourceTestTrait;
use Drupal\Tests\rest\Functional\EntityResource\XmlEntityNormalizationQuirksTrait;
/**
* @group rest
*/
class WorkflowXmlAnonTest extends WorkflowResourceTestBase {
use AnonResourceTestTrait;
use XmlEntityNormalizationQuirksTrait;
/**
* {@inheritdoc}
*/
protected static $format = 'xml';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'text/xml; charset=UTF-8';
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
}

View File

@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\workflows\Functional\Rest;
use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
use Drupal\Tests\rest\Functional\EntityResource\XmlEntityNormalizationQuirksTrait;
/**
* @group rest
*/
class WorkflowXmlBasicAuthTest extends WorkflowResourceTestBase {
use BasicAuthResourceTestTrait;
use XmlEntityNormalizationQuirksTrait;
/**
* {@inheritdoc}
*/
protected static $modules = ['basic_auth'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected static $format = 'xml';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'text/xml; charset=UTF-8';
/**
* {@inheritdoc}
*/
protected static $auth = 'basic_auth';
}

View File

@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\workflows\Functional\Rest;
use Drupal\Tests\rest\Functional\CookieResourceTestTrait;
use Drupal\Tests\rest\Functional\EntityResource\XmlEntityNormalizationQuirksTrait;
/**
* @group rest
*/
class WorkflowXmlCookieTest extends WorkflowResourceTestBase {
use CookieResourceTestTrait;
use XmlEntityNormalizationQuirksTrait;
/**
* {@inheritdoc}
*/
protected static $format = 'xml';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'text/xml; charset=UTF-8';
/**
* {@inheritdoc}
*/
protected static $auth = 'cookie';
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
}

View File

@@ -0,0 +1,94 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\workflows\Functional;
use Drupal\Tests\BrowserTestBase;
use Drupal\workflows\Entity\Workflow;
/**
* Test custom provided workflow access for state/transition operations.
*
* @group workflows
*/
class WorkflowCustomStateTransitionAccessTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'workflows',
'workflow_type_test',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* A test admin user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $adminUser;
/**
* A test workflow.
*
* @var \Drupal\workflows\WorkflowInterface
*/
protected $testWorkflow;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->adminUser = $this->createUser(['administer workflows']);
$this->testWorkflow = Workflow::create([
'label' => 'Test workflow',
'id' => 'test_type',
'type' => 'workflow_custom_access_type',
]);
$this->testWorkflow->save();
}
/**
* Tests the custom state/transition operation access rules.
*/
public function testCustomWorkflowAccessOperations(): void {
$this->drupalLogin($this->adminUser);
$forbidden_paths = [
'admin/config/workflow/workflows/manage/test_type/state/cannot_delete/delete',
'admin/config/workflow/workflows/manage/test_type/state/cannot_update',
'admin/config/workflow/workflows/manage/test_type/transition/cannot_update',
'admin/config/workflow/workflows/manage/test_type/transition/cannot_delete/delete',
'admin/config/workflow/workflows/manage/test_type/add_state',
'admin/config/workflow/workflows/manage/test_type/add_transition',
];
// Until the list of forbidden operations have been set, the admin user
// should be able to access all the forbidden paths.
foreach ($forbidden_paths as $forbidden_path) {
$this->drupalGet($forbidden_path);
$this->assertSession()->statusCodeEquals(200);
}
// Update the forbidden operations which deny access to the actions
// represented by the above paths.
$this->container->get('state')->set('workflow_type_test_forbidden_operations', [
'update-state:cannot_update',
'delete-state:cannot_delete',
'update-transition:cannot_update',
'delete-transition:cannot_delete',
'add-state',
'add-transition',
]);
foreach ($forbidden_paths as $forbidden_path) {
$this->drupalGet($forbidden_path);
$this->assertSession()->statusCodeEquals(403);
}
}
}

View File

@@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\workflows\Functional;
use Drupal\Core\Url;
use Drupal\Tests\BrowserTestBase;
/**
* Tests workflow UI when there are no types.
*
* @group workflows
*/
class WorkflowUiNoTypeTest extends BrowserTestBase {
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['workflows', 'block'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// We're testing local actions.
$this->drupalPlaceBlock('local_actions_block');
}
/**
* Tests the creation of a workflow through the UI.
*/
public function testWorkflowUiWithNoType(): void {
$this->drupalLogin($this->createUser(['access administration pages', 'administer workflows']));
$this->drupalGet('admin/config/workflow/workflows/add');
// There are no workflow types so this should be a 403.
$this->assertSession()->statusCodeEquals(403);
$this->drupalGet('admin/config/workflow/workflows');
$this->assertSession()->pageTextContains('There are no workflow types available. In order to create workflows you need to install a module that provides a workflow type. For example, the Content Moderation module provides a workflow type that enables workflows for content entities.');
$this->assertSession()->linkExists('Content Moderation');
$this->assertSession()->pageTextNotContains('Add workflow');
$this->clickLink('Content Moderation');
$modules_list_url_absolute = Url::fromRoute('system.modules_list', [], [
'fragment' => 'module-content-moderation',
'absolute' => TRUE,
])->toString();
$this->assertSame($this->getSession()->getCurrentUrl(), $modules_list_url_absolute);
// The current user does not have the 'administer modules' permission.
$this->assertSession()->statusCodeEquals(403);
$this->container->get('module_installer')->install(['workflow_type_test']);
// The render cache needs to be cleared because although the cache tags are
// correctly set the render cache does not pick it up.
\Drupal::cache('render')->deleteAll();
$this->drupalGet('admin/config/workflow/workflows');
$this->assertSession()->pageTextNotContains('There are no workflow types available. In order to create workflows you need to install a module that provides a workflow type. For example, the Content Moderation module provides a workflow type that enables workflows for content entities.');
$this->assertSession()->linkExists('Add workflow');
$this->assertSession()->pageTextContains('There are no workflows yet.');
}
}

View File

@@ -0,0 +1,427 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\workflows\Functional;
use Drupal\Core\Url;
use Drupal\Tests\BrowserTestBase;
use Drupal\workflows\Entity\Workflow;
/**
* Tests workflow creation UI.
*
* @group workflows
* @group #slow
*/
class WorkflowUiTest extends BrowserTestBase {
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['workflows', 'workflow_type_test', 'block'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// We're testing local actions.
$this->drupalPlaceBlock('local_actions_block');
}
/**
* Tests route access/permissions.
*/
public function testAccess(): void {
// Create a minimal workflow for testing.
$workflow = Workflow::create(['id' => 'test', 'type' => 'workflow_type_test', 'label' => 'Test']);
$workflow
->getTypePlugin()
->addState('draft', 'Draft')
->addState('published', 'Published')
->addTransition('publish', 'Publish', ['draft', 'published'], 'published');
$workflow->save();
$paths = [
'admin/config/workflow/workflows',
'admin/config/workflow/workflows/add',
'admin/config/workflow/workflows/manage/test',
'admin/config/workflow/workflows/manage/test/delete',
'admin/config/workflow/workflows/manage/test/add_state',
'admin/config/workflow/workflows/manage/test/state/published',
'admin/config/workflow/workflows/manage/test/state/published/delete',
'admin/config/workflow/workflows/manage/test/add_transition',
'admin/config/workflow/workflows/manage/test/transition/publish',
'admin/config/workflow/workflows/manage/test/transition/publish/delete',
];
foreach ($paths as $path) {
$this->drupalGet($path);
// No access.
$this->assertSession()->statusCodeEquals(403);
}
$this->drupalLogin($this->createUser(['administer workflows']));
foreach ($paths as $path) {
$this->drupalGet($path);
// User has access.
$this->assertSession()->statusCodeEquals(200);
}
// Ensure that default states can not be deleted.
\Drupal::state()->set('workflow_type_test.required_states', ['published']);
$this->drupalGet('admin/config/workflow/workflows/manage/test/state/published/delete');
$this->assertSession()->statusCodeEquals(403);
\Drupal::state()->set('workflow_type_test.required_states', []);
// Delete one of the states and ensure the other test cannot be deleted.
$this->drupalGet('admin/config/workflow/workflows/manage/test/state/published/delete');
$this->submitForm([], 'Delete');
$this->drupalGet('admin/config/workflow/workflows/manage/test/state/draft/delete');
$this->assertSession()->statusCodeEquals(403);
}
/**
* Tests the machine name validation of the state add form.
*/
public function testStateMachineNameValidation(): void {
Workflow::create([
'id' => 'test_workflow',
'label' => 'Test workflow',
'type' => 'workflow_type_test',
])->save();
$this->drupalLogin($this->createUser(['administer workflows']));
$this->drupalGet('admin/config/workflow/workflows/manage/test_workflow/add_state');
$this->submitForm([
'label' => 'Test State',
'id' => 'Invalid ID',
], 'Save');
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->pageTextContains('The machine-readable name must contain only lowercase letters, numbers, and underscores.');
$this->drupalGet('admin/config/workflow/workflows/manage/test_workflow/add_transition');
$this->submitForm([
'label' => 'Test Transition',
'id' => 'Invalid ID',
], 'Save');
$this->assertSession()->pageTextContains('The machine-readable name must contain only lowercase letters, numbers, and underscores.');
}
/**
* Tests the creation of a workflow through the UI.
*/
public function testWorkflowCreation(): void {
$workflow_storage = $this->container->get('entity_type.manager')->getStorage('workflow');
/** @var \Drupal\workflows\WorkflowInterface $workflow */
$this->drupalLogin($this->createUser(['access administration pages', 'administer workflows']));
$this->drupalGet('admin/config/workflow');
$this->assertSession()->linkByHrefExists('admin/config/workflow/workflows');
$this->clickLink('Workflows');
$this->assertSession()->pageTextContains('Workflows');
$this->assertSession()->pageTextContains('There are no workflows yet.');
$this->clickLink('Add workflow');
$this->submitForm(['label' => 'Test', 'id' => 'test', 'workflow_type' => 'workflow_type_test'], 'Save');
$this->assertSession()->pageTextContains('Created the Test Workflow.');
$this->assertSession()->addressEquals('admin/config/workflow/workflows/manage/test/add_state');
$this->drupalGet('/admin/config/workflow/workflows/manage/test');
$this->assertSession()->pageTextContains('This workflow has no states and will be disabled until there is at least one, add a new state.');
$this->assertSession()->pageTextContains('There are no states yet.');
$this->clickLink('Add a new state');
$this->submitForm(['label' => 'Published', 'id' => 'published'], 'Save');
$this->assertSession()->pageTextContains('Created Published state.');
$workflow = $workflow_storage->loadUnchanged('test');
$this->assertFalse($workflow->getTypePlugin()->getState('published')->canTransitionTo('published'), 'No default transition from published to published exists.');
$this->clickLink('Add a new state');
// Don't create a draft to draft transition by default.
$this->submitForm(['label' => 'Draft', 'id' => 'draft'], 'Save');
$this->assertSession()->pageTextContains('Created Draft state.');
$workflow = $workflow_storage->loadUnchanged('test');
$this->assertFalse($workflow->getTypePlugin()->getState('draft')->canTransitionTo('draft'), 'Can not transition from draft to draft');
$this->clickLink('Add a new transition');
$this->submitForm(['id' => 'publish', 'label' => 'Publish', 'from[draft]' => 'draft', 'to' => 'published'], 'Save');
$this->assertSession()->pageTextContains('Created Publish transition.');
$workflow = $workflow_storage->loadUnchanged('test');
$this->assertTrue($workflow->getTypePlugin()->getState('draft')->canTransitionTo('published'), 'Can transition from draft to published');
$this->clickLink('Add a new transition');
$this->assertCount(2, $this->cssSelect('input[name="to"][type="radio"]'));
$this->assertCount(0, $this->cssSelect('input[name="to"][checked="checked"][type="radio"]'));
$this->submitForm(['id' => 'create_new_draft', 'label' => 'Create new draft', 'from[draft]' => 'draft', 'to' => 'draft'], 'Save');
$this->assertSession()->pageTextContains('Created Create new draft transition.');
$workflow = $workflow_storage->loadUnchanged('test');
$this->assertTrue($workflow->getTypePlugin()->getState('draft')->canTransitionTo('draft'), 'Can transition from draft to draft');
// The fist state to edit on the page should be published.
$this->clickLink('Edit');
$this->assertSession()->fieldValueEquals('label', 'Published');
// Change the label.
$this->submitForm(['label' => 'Live'], 'Save');
$this->assertSession()->pageTextContains('Saved Live state.');
// Allow published to draft.
$this->clickLink('Edit', 3);
$this->submitForm(['from[published]' => 'published'], 'Save');
$this->assertSession()->pageTextContains('Saved Create new draft transition.');
$workflow = $workflow_storage->loadUnchanged('test');
$this->assertTrue($workflow->getTypePlugin()->getState('published')->canTransitionTo('draft'), 'Can transition from published to draft');
// Try creating a duplicate transition.
$this->clickLink('Add a new transition');
$this->submitForm(['id' => 'create_new_draft', 'label' => 'Create new draft', 'from[published]' => 'published', 'to' => 'draft'], 'Save');
$this->assertSession()->pageTextContains('The machine-readable name is already in use. It must be unique.');
// Try creating a transition which duplicates the states of another.
$this->submitForm(['id' => 'create_new_draft2', 'label' => 'Create new draft again', 'from[published]' => 'published', 'to' => 'draft'], 'Save');
$this->assertSession()->pageTextContains('The transition from Live to Draft already exists.');
// Create a new transition.
$this->submitForm(['id' => 'save_and_publish', 'label' => 'Save and publish', 'from[published]' => 'published', 'to' => 'published'], 'Save');
$this->assertSession()->pageTextContains('Created Save and publish transition.');
// Edit the new transition and try to add an existing transition.
$this->clickLink('Edit', 4);
$this->submitForm(['from[draft]' => 'draft'], 'Save');
$this->assertSession()->pageTextContains('The transition from Draft to Live already exists.');
// Delete the transition.
$workflow = $workflow_storage->loadUnchanged('test');
$this->assertTrue($workflow->getTypePlugin()->hasTransitionFromStateToState('published', 'published'), 'Can transition from published to published');
$this->clickLink('Delete');
$this->assertSession()->pageTextContains('Are you sure you want to delete Save and publish from Test?');
$this->submitForm([], 'Delete');
$workflow = $workflow_storage->loadUnchanged('test');
$this->assertFalse($workflow->getTypePlugin()->hasTransitionFromStateToState('published', 'published'), 'Cannot transition from published to published');
// Try creating a duplicate state.
$this->drupalGet('admin/config/workflow/workflows/manage/test');
$this->clickLink('Add a new state');
$this->submitForm(['label' => 'Draft', 'id' => 'draft'], 'Save');
$this->assertSession()->pageTextContains('The machine-readable name is already in use. It must be unique.');
// Ensure that weight changes the state ordering.
$workflow = $workflow_storage->loadUnchanged('test');
$this->assertEquals('published', $workflow->getTypePlugin()->getInitialState()->id());
$this->drupalGet('admin/config/workflow/workflows/manage/test');
$this->submitForm(['states[draft][weight]' => '-1'], 'Save');
$workflow = $workflow_storage->loadUnchanged('test');
$this->assertEquals('draft', $workflow->getTypePlugin()->getInitialState()->id());
// Verify that we are still on the workflow edit page.
$this->assertSession()->addressEquals('admin/config/workflow/workflows/manage/test');
// Ensure that weight changes the transition ordering.
$this->assertEquals(['publish', 'create_new_draft'], array_keys($workflow->getTypePlugin()->getTransitions()));
$this->drupalGet('admin/config/workflow/workflows/manage/test');
$this->submitForm(['transitions[create_new_draft][weight]' => '-1'], 'Save');
$workflow = $workflow_storage->loadUnchanged('test');
$this->assertEquals(['create_new_draft', 'publish'], array_keys($workflow->getTypePlugin()->getTransitions()));
// Verify that we are still on the workflow edit page.
$this->assertSession()->addressEquals('admin/config/workflow/workflows/manage/test');
// Ensure that a delete link for the published state exists before deleting
// the draft state.
$published_delete_link = Url::fromRoute('entity.workflow.delete_state_form', [
'workflow' => $workflow->id(),
'workflow_state' => 'published',
])->toString();
$draft_delete_link = Url::fromRoute('entity.workflow.delete_state_form', [
'workflow' => $workflow->id(),
'workflow_state' => 'draft',
])->toString();
$this->assertSession()->elementContains('css', 'tr[data-drupal-selector="edit-states-published"]', 'Delete');
$this->assertSession()->linkByHrefExists($published_delete_link);
$this->assertSession()->linkByHrefExists($draft_delete_link);
// Make the published state a default state and ensure it is no longer
// linked.
\Drupal::state()->set('workflow_type_test.required_states', ['published']);
$this->getSession()->reload();
$this->assertSession()->linkByHrefNotExists($published_delete_link);
$this->assertSession()->linkByHrefExists($draft_delete_link);
$this->assertSession()->elementNotContains('css', 'tr[data-drupal-selector="edit-states-published"]', 'Delete');
\Drupal::state()->set('workflow_type_test.required_states', []);
$this->getSession()->reload();
$this->assertSession()->elementContains('css', 'tr[data-drupal-selector="edit-states-published"]', 'Delete');
$this->assertSession()->linkByHrefExists($published_delete_link);
$this->assertSession()->linkByHrefExists($draft_delete_link);
// Delete the Draft state.
$this->clickLink('Delete');
$this->assertSession()->pageTextContains('Are you sure you want to delete Draft from Test?');
$this->submitForm([], 'Delete');
$this->assertSession()->pageTextContains('State Draft deleted.');
$workflow = $workflow_storage->loadUnchanged('test');
$this->assertFalse($workflow->getTypePlugin()->hasState('draft'), 'Draft state deleted');
$this->assertTrue($workflow->getTypePlugin()->hasState('published'), 'Workflow still has published state');
// The last state cannot be deleted so the only delete link on the page will
// be for the workflow.
$this->assertSession()->linkByHrefNotExists($published_delete_link);
$this->clickLink('Delete');
$this->assertSession()->pageTextContains('Are you sure you want to delete Test?');
$this->submitForm([], 'Delete');
$this->assertSession()->pageTextContains('Workflow Test deleted.');
$this->assertSession()->pageTextContains('There are no workflows yet.');
$this->assertNull($workflow_storage->loadUnchanged('test'), 'The test workflow has been deleted');
// Ensure that workflow types with default configuration are initialized
// correctly.
$this->drupalGet('admin/config/workflow/workflows');
$this->clickLink('Add workflow');
$this->submitForm(['label' => 'Test 2', 'id' => 'test2', 'workflow_type' => 'workflow_type_required_state_test'], 'Save');
$this->assertSession()->addressEquals('admin/config/workflow/workflows/manage/test2');
$workflow = $workflow_storage->loadUnchanged('test2');
$this->assertTrue($workflow->getTypePlugin()->hasState('fresh'), 'The workflow has the "fresh" state');
$this->assertTrue($workflow->getTypePlugin()->hasState('rotten'), 'The workflow has the "rotten" state');
$this->assertTrue($workflow->getTypePlugin()->hasTransition('rot'), 'The workflow has the "rot" transition');
$this->assertSession()->pageTextContains('Fresh');
$this->assertSession()->pageTextContains('Rotten');
}
/**
* Tests the workflow configuration form.
*/
public function testWorkflowConfigurationForm(): void {
$workflow = Workflow::create(['id' => 'test', 'type' => 'workflow_type_complex_test', 'label' => 'Test']);
$workflow
->getTypePlugin()
->addState('published', 'Published')
->addTransition('publish', 'Publish', ['published'], 'published');
$workflow->save();
$this->drupalLogin($this->createUser(['administer workflows']));
// Add additional information to the workflow via the configuration form.
$this->drupalGet('admin/config/workflow/workflows/manage/test');
$this->assertSession()->pageTextContains('Example global workflow setting');
$this->submitForm(['type_settings[example_setting]' => 'Extra global settings'], 'Save');
$workflow_storage = $this->container->get('entity_type.manager')->getStorage('workflow');
$workflow = $workflow_storage->loadUnchanged('test');
$this->assertEquals('Extra global settings', $workflow->getTypePlugin()->getConfiguration()['example_setting']);
}
/**
* Tests a workflow, state, and transition can have a numeric ID and label.
*/
public function testNumericIds(): void {
$this->drupalLogin($this->createUser(['administer workflows']));
$this->drupalGet('admin/config/workflow/workflows');
$this->clickLink('Add workflow');
$this->submitForm(['label' => 123, 'id' => 123, 'workflow_type' => 'workflow_type_complex_test'], 'Save');
$this->assertSession()->addressEquals('admin/config/workflow/workflows/manage/123/add_state');
$this->submitForm(['label' => 456, 'id' => 456], 'Save');
$this->assertSession()->pageTextContains('Created 456 state.');
$this->clickLink('Add a new state');
$this->submitForm(['label' => 789, 'id' => 789], 'Save');
$this->assertSession()->pageTextContains('Created 789 state.');
$this->clickLink('Add a new transition');
$this->submitForm(['id' => 101112, 'label' => 101112, 'from[456]' => 456, 'to' => 789], 'Save');
$this->assertSession()->pageTextContains('Created 101112 transition.');
$workflow = $this->container->get('entity_type.manager')->getStorage('workflow')->loadUnchanged(123);
$this->assertEquals(123, $workflow->id());
$this->assertEquals(456, $workflow->getTypePlugin()->getState(456)->id());
$this->assertEquals(101112, $workflow->getTypePlugin()->getTransition(101112)->id());
$this->assertEquals(789, $workflow->getTypePlugin()->getTransition(101112)->to()->id());
}
/**
* Tests the sorting of states and transitions by weight and label.
*/
public function testSorting(): void {
$workflow = Workflow::create(['id' => 'test', 'type' => 'workflow_type_complex_test', 'label' => 'Test']);
$workflow
->getTypePlugin()
->setConfiguration([
'states' => [
'two_a' => [
'label' => 'two a',
'weight' => 2,
],
'three' => [
'label' => 'three',
'weight' => 3,
],
'two_b' => [
'label' => 'two b',
'weight' => 2,
],
'one' => [
'label' => 'one',
'weight' => 1,
],
],
'transitions' => [
'three' => [
'label' => 'three',
'from' => ['three'],
'to' => 'three',
'weight' => 3,
],
'two_a' => [
'label' => 'two a',
'from' => ['two_a'],
'to' => 'two_a',
'weight' => 2,
],
'one' => [
'label' => 'one',
'from' => ['one'],
'to' => 'one',
'weight' => 1,
],
'two_b' => [
'label' => 'two b',
'from' => ['two_b'],
'to' => 'two_b',
'weight' => 2,
],
],
]);
$workflow->save();
$this->drupalLogin($this->createUser(['administer workflows']));
$this->drupalGet('admin/config/workflow/workflows/manage/test');
$expected_states = ['one', 'two a', 'two b', 'three'];
$elements = $this->xpath('//details[@id="edit-states-container"]//table/tbody/tr');
foreach ($elements as $key => $element) {
$this->assertEquals($expected_states[$key], $element->find('xpath', 'td')->getText());
}
$expected_transitions = ['one', 'two a', 'two b', 'three'];
$elements = $this->xpath('//details[@id="edit-transitions-container"]//table/tbody/tr');
foreach ($elements as $key => $element) {
$this->assertEquals($expected_transitions[$key], $element->find('xpath', 'td')->getText());
}
// Ensure that there are enough weights to satisfy the potential number of
// states and transitions.
$this->assertSession()
->selectExists('states[three][weight]')
->selectOption('2');
$this->assertSession()
->selectExists('states[three][weight]')
->selectOption('-2');
$this->assertSession()
->selectExists('transitions[three][weight]')
->selectOption('2');
$this->assertSession()
->selectExists('transitions[three][weight]')
->selectOption('-2');
}
}

View File

@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\workflows\Kernel;
use Drupal\KernelTests\KernelTestBase;
use Drupal\workflows\Entity\Workflow;
/**
* Workflow entity tests that require modules or storage.
*
* @coversDefaultClass \Drupal\workflow_type_test\Plugin\WorkflowType\ComplexTestType
*
* @group workflows
*/
class ComplexWorkflowTypeTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['workflows', 'workflow_type_test'];
/**
* @covers \Drupal\workflows\Entity\Workflow::loadMultipleByType
*/
public function testLoadMultipleByType(): void {
$workflow1 = Workflow::create([
'id' => 'test1',
'label' => 'Test 1',
'type' => 'workflow_type_complex_test',
]);
$workflow1->save();
$workflow2 = Workflow::create([
'id' => 'test2',
'label' => 'Test 2',
'type' => 'workflow_type_complex_test',
]);
$workflow2->save();
$workflow3 = Workflow::create([
'id' => 'test3',
'label' => 'Test 3',
'type' => 'workflow_type_test',
]);
$workflow3->save();
$this->assertEquals(['test1', 'test2'], array_keys(Workflow::loadMultipleByType('workflow_type_complex_test')));
$this->assertEquals(['test3'], array_keys(Workflow::loadMultipleByType('workflow_type_test')));
$this->assertEquals([], Workflow::loadMultipleByType('a_type_that_does_not_exist'));
}
}

View File

@@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\workflows\Kernel;
use Drupal\KernelTests\KernelTestBase;
use Drupal\workflows\Entity\Workflow;
/**
* Test a predefined workflow based on something other than configuration.
*
* @group workflows
*/
class PredefinedWorkflowTypeTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['workflows', 'workflow_type_test'];
/**
* Tests a predefined workflow type.
*/
public function testPredefinedWorkflowType(): void {
$workflow = Workflow::create([
'id' => 'aces',
'label' => 'Aces Workflow',
'type' => 'predefined_states_workflow_test_type',
'transitions' => [
'bet' => [
'label' => 'Bet',
'from' => [
'pay_blinds',
],
'to' => 'bet',
],
'raise' => [
'label' => 'Raise',
'from' => [
'pay_blinds',
],
'to' => 'raise',
],
],
]);
$workflow->save();
// No states configuration is stored for this workflow.
$configuration = $workflow->getTypePlugin()->getConfiguration();
$this->assertFalse(isset($configuration['states']));
}
}

View File

@@ -0,0 +1,132 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\workflows\Kernel;
use Drupal\KernelTests\KernelTestBase;
use Drupal\workflows\Entity\Workflow;
use Drupal\workflows\Exception\RequiredStateMissingException;
/**
* Tests Workflow type's required states and configuration initialization.
*
* @coversDefaultClass \Drupal\workflows\Plugin\WorkflowTypeBase
*
* @group workflows
*/
class RequiredStatesTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['workflows', 'workflow_type_test'];
/**
* @covers ::getRequiredStates
* @covers ::__construct
*/
public function testGetRequiredStates(): void {
$workflow = Workflow::create([
'id' => 'test',
'label' => 'Test workflow',
'type' => 'workflow_type_required_state_test',
]);
$workflow->save();
$this->assertEquals(['fresh', 'rotten'], $workflow->getTypePlugin()
->getRequiredStates());
// Ensure that the workflow has the default configuration.
$this->assertTrue($workflow->getTypePlugin()->hasState('rotten'));
$this->assertTrue($workflow->getTypePlugin()->hasState('fresh'));
$this->assertTrue($workflow->getTypePlugin()->hasTransitionFromStateToState('fresh', 'rotten'));
}
/**
* @covers \Drupal\workflows\Entity\Workflow::preSave
*/
public function testDeleteRequiredStateAPI(): void {
$workflow = Workflow::create([
'id' => 'test',
'label' => 'Test workflow',
'type' => 'workflow_type_required_state_test',
]);
$workflow->save();
// Ensure that required states can't be deleted.
$this->expectException(RequiredStateMissingException::class);
$this->expectExceptionMessage("Required State Type Test' requires states with the ID 'fresh' in workflow 'test'");
$workflow->getTypePlugin()->deleteState('fresh');
$workflow->save();
}
/**
* @covers \Drupal\workflows\Entity\Workflow::preSave
*/
public function testNoStatesRequiredStateAPI(): void {
$workflow = Workflow::create([
'id' => 'test',
'type' => 'workflow_type_required_state_test',
'type_settings' => [
'states' => [],
],
]);
$this->expectException(RequiredStateMissingException::class);
$this->expectExceptionMessage("Required State Type Test' requires states with the ID 'fresh', 'rotten' in workflow 'test'");
$workflow->save();
}
/**
* Ensures that initialized configuration can be changed.
*/
public function testChangeRequiredStateAPI(): void {
$workflow = Workflow::create([
'id' => 'test',
'label' => 'Test workflow',
'type' => 'workflow_type_required_state_test',
]);
$workflow->save();
// Ensure states added by default configuration can be changed.
$this->assertEquals('Fresh', $workflow->getTypePlugin()->getState('fresh')->label());
$workflow
->getTypePlugin()
->setStateLabel('fresh', 'Fresher');
$workflow->save();
$this->assertEquals('Fresher', $workflow->getTypePlugin()->getState('fresh')->label());
// Ensure transitions can be altered.
$workflow
->getTypePlugin()
->addState('cooked', 'Cooked')
->setTransitionFromStates('rot', ['fresh', 'cooked']);
$workflow->save();
$this->assertTrue($workflow->getTypePlugin()->hasTransitionFromStateToState('fresh', 'rotten'));
$this->assertTrue($workflow->getTypePlugin()->hasTransitionFromStateToState('cooked', 'rotten'));
$workflow
->getTypePlugin()
->setTransitionFromStates('rot', ['cooked']);
$workflow->save();
$this->assertFalse($workflow->getTypePlugin()->hasTransitionFromStateToState('fresh', 'rotten'));
$this->assertTrue($workflow->getTypePlugin()->hasTransitionFromStateToState('cooked', 'rotten'));
// Ensure the default configuration does not cause ordering issues.
$workflow->getTypePlugin()->addTransition('cook', 'Cook', ['fresh'], 'cooked');
$workflow->save();
$this->assertSame([
'cooked',
'fresh',
'rotten',
], array_keys($workflow->getTypePlugin()->getConfiguration()['states']));
$this->assertSame([
'cook',
'rot',
], array_keys($workflow->getTypePlugin()->getConfiguration()['transitions']));
// Ensure that transitions can be deleted.
$workflow->getTypePlugin()->deleteTransition('rot');
$workflow->save();
$this->assertFalse($workflow->getTypePlugin()->hasTransition('rot'));
}
}

View File

@@ -0,0 +1,278 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\workflows\Kernel;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Cache\Context\CacheContextsManager;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\KernelTests\KernelTestBase;
use Drupal\Tests\user\Traits\UserCreationTrait;
use Drupal\workflows\Entity\Workflow;
use Prophecy\Prophet;
/**
* @coversDefaultClass \Drupal\workflows\WorkflowAccessControlHandler
* @group workflows
* @group #slow
*/
class WorkflowAccessControlHandlerTest extends KernelTestBase {
use UserCreationTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'workflows',
'workflow_type_test',
'system',
'user',
];
/**
* The workflow access control handler.
*
* @var \Drupal\workflows\WorkflowAccessControlHandler
*/
protected $accessControlHandler;
/**
* A test admin user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $adminUser;
/**
* A non-privileged user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $user;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installEntitySchema('user');
$this->accessControlHandler = $this->container->get('entity_type.manager')->getAccessControlHandler('workflow');
// Create and discard user 1, which is special and bypasses all access
// checking.
$this->createUser([]);
$this->user = $this->createUser([]);
$this->adminUser = $this->createUser(['administer workflows']);
}
/**
* @covers ::checkCreateAccess
*/
public function testCheckCreateAccess(): void {
// A user must have the correct permission to create a workflow.
$this->assertEquals(
AccessResult::neutral()
->addCacheContexts(['user.permissions'])
->setReason("The 'administer workflows' permission is required."),
$this->accessControlHandler->createAccess(NULL, $this->user, [], TRUE)
);
$this->assertEquals(
AccessResult::allowed()
->addCacheContexts(['user.permissions']),
$this->accessControlHandler->createAccess(NULL, $this->adminUser, [], TRUE)
);
// Remove all plugin types and ensure not even the admin user is allowed to
// create a workflow.
workflow_type_test_set_definitions([]);
$this->accessControlHandler->resetCache();
$this->assertEquals(
AccessResult::neutral()
->addCacheContexts(['user.permissions']),
$this->accessControlHandler->createAccess(NULL, $this->adminUser, [], TRUE)
);
}
/**
* @covers ::checkAccess
* @dataProvider checkAccessProvider
*/
public function testCheckAccess($user, $operation, $result, $states_to_create = []): void {
$workflow = Workflow::create([
'type' => 'workflow_type_test',
'id' => 'test_workflow',
'label' => 'Test workflow',
]);
$workflow->save();
$workflow_type = $workflow->getTypePlugin();
foreach ($states_to_create as $state_id => $is_required) {
$workflow_type->addState($state_id, $this->randomString());
}
\Drupal::state()->set('workflow_type_test.required_states', array_filter($states_to_create));
$this->assertEquals($result, $this->accessControlHandler->access($workflow, $operation, $this->{$user}, TRUE));
}
/**
* Data provider for ::testCheckAccess.
*
* @return array
*/
public static function checkAccessProvider() {
$container = new ContainerBuilder();
$cache_contexts_manager = (new Prophet())->prophesize(CacheContextsManager::class);
$cache_contexts_manager->assertValidTokens()->willReturn(TRUE);
$cache_contexts_manager->reveal();
$container->set('cache_contexts_manager', $cache_contexts_manager);
\Drupal::setContainer($container);
return [
'Admin view' => [
'adminUser',
'view',
AccessResult::allowed()->addCacheContexts(['user.permissions']),
],
'Admin update' => [
'adminUser',
'update',
AccessResult::allowed()->addCacheContexts(['user.permissions']),
],
'Admin delete' => [
'adminUser',
'delete',
AccessResult::allowed()->addCacheContexts(['user.permissions']),
],
'Admin delete only state' => [
'adminUser',
'delete-state:foo',
AccessResult::neutral()->addCacheTags(['config:workflows.workflow.test_workflow']),
['foo' => FALSE],
],
'Admin delete one of two states' => [
'adminUser',
'delete-state:foo',
AccessResult::allowed()
->addCacheTags(['config:workflows.workflow.test_workflow'])
->addCacheContexts(['user.permissions']),
['foo' => FALSE, 'bar' => FALSE],
],
'Admin delete required state when there are >1 states' => [
'adminUser',
'delete-state:foo',
AccessResult::allowed()
->addCacheTags(['config:workflows.workflow.test_workflow'])
->addCacheContexts(['user.permissions']),
['foo' => TRUE, 'bar' => FALSE],
],
'User view' => [
'user',
'view',
AccessResult::neutral()
->addCacheContexts(['user.permissions'])
->setReason("The 'administer workflows' permission is required."),
],
'User update' => [
'user',
'update',
AccessResult::neutral()
->addCacheContexts(['user.permissions'])
->setReason("The 'administer workflows' permission is required."),
],
'User delete' => [
'user',
'delete',
AccessResult::neutral()
->addCacheContexts(['user.permissions'])
->setReason("The 'administer workflows' permission is required."),
],
'User delete only state' => [
'user',
'delete-state:foo',
AccessResult::neutral()->addCacheTags(['config:workflows.workflow.test_workflow']),
['foo' => FALSE],
],
'User delete one of two states' => [
'user',
'delete-state:foo',
AccessResult::neutral()
->addCacheTags(['config:workflows.workflow.test_workflow'])
->addCacheContexts(['user.permissions'])
->setReason("The 'administer workflows' permission is required."),
['foo' => FALSE, 'bar' => FALSE],
],
'User delete required state when there are >1 states' => [
'user',
'delete-state:foo',
AccessResult::neutral()
->addCacheTags(['config:workflows.workflow.test_workflow'])
->addCacheContexts(['user.permissions'])
->setReason("The 'administer workflows' permission is required."),
['foo' => TRUE, 'bar' => FALSE],
],
'Update state for user, uses admin permission by default' => [
'user',
'update-state:foo',
AccessResult::neutral()
->addCacheContexts(['user.permissions'])
->setReason("The 'administer workflows' permission is required."),
],
'Update state for admin, uses admin permission by default' => [
'adminUser',
'update-state:foo',
AccessResult::allowed()->addCacheContexts(['user.permissions']),
],
'Add state for user, uses admin permission by default' => [
'user',
'add-state',
AccessResult::neutral()
->addCacheContexts(['user.permissions'])
->setReason("The 'administer workflows' permission is required."),
],
'Add state for admin, uses admin permission by default' => [
'adminUser',
'add-state',
AccessResult::allowed()->addCacheContexts(['user.permissions']),
],
'Add transition for user, uses admin permission by default' => [
'user',
'add-transition',
AccessResult::neutral()
->addCacheContexts(['user.permissions'])
->setReason("The 'administer workflows' permission is required."),
],
'Add transition for admin, uses admin permission by default' => [
'adminUser',
'add-transition',
AccessResult::allowed()->addCacheContexts(['user.permissions']),
],
'Edit transition for user, uses admin permission by default' => [
'user',
'edit-transition:foo',
AccessResult::neutral()
->addCacheContexts(['user.permissions'])
->setReason("The 'administer workflows' permission is required."),
],
'Edit transition for admin, uses admin permission by default' => [
'adminUser',
'edit-transition:foo',
AccessResult::allowed()->addCacheContexts(['user.permissions']),
],
'Delete transition for user, uses admin permission by default' => [
'user',
'delete-transition:foo',
AccessResult::neutral()
->addCacheContexts(['user.permissions'])
->setReason("The 'administer workflows' permission is required."),
],
'Delete transition for admin, uses admin permission by default' => [
'adminUser',
'delete-transition:foo',
AccessResult::allowed()->addCacheContexts(['user.permissions']),
],
];
}
}

View File

@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\workflows\Kernel;
use Drupal\KernelTests\KernelTestBase;
use Drupal\workflows\Entity\Workflow;
/**
* Tests configuration dependencies in workflows.
*
* @coversDefaultClass \Drupal\workflows\Entity\Workflow
*
* @group workflows
*/
class WorkflowDependenciesTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'system',
'workflows',
'workflow_type_test',
'workflow_third_party_settings_test',
];
/**
* Tests \Drupal\workflows\Entity\Workflow::onDependencyRemoval().
*/
public function testOnDependencyRemoval(): void {
// Create a workflow that has a dependency on a third party setting.
$workflow = Workflow::create([
'id' => 'test3',
'label' => 'Test workflow',
'type' => 'workflow_type_complex_test',
]);
$workflow->setThirdPartySetting('workflow_third_party_settings_test', 'key', 'value');
$workflow->save();
$this->assertSame(['workflow_third_party_settings_test', 'workflow_type_test'], $workflow->getDependencies()['module']);
// Uninstall workflow_third_party_settings_test to ensure
// \Drupal\workflows\Entity\Workflow::onDependencyRemoval() works as
// expected.
\Drupal::service('module_installer')->uninstall(['node', 'workflow_third_party_settings_test']);
/** @var \Drupal\workflows\WorkflowInterface $workflow */
$workflow = \Drupal::entityTypeManager()->getStorage('workflow')->loadUnchanged($workflow->id());
$this->assertSame(['workflow_type_test'], $workflow->getDependencies()['module']);
}
}

View File

@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\workflows\Kernel;
use Drupal\KernelTests\Core\Config\ConfigEntityValidationTestBase;
use Drupal\workflows\Entity\Workflow;
/**
* Tests validation of workflow entities.
*
* @group workflows
* @group #slow
*/
class WorkflowValidationTest extends ConfigEntityValidationTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['workflows', 'workflow_type_test'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->entity = Workflow::create([
'id' => 'test',
'label' => 'Test',
'type' => 'workflow_type_test',
]);
$this->entity->save();
}
/**
* Tests that the workflow type plugin is validated.
*/
public function testTypePluginIsValidated(): void {
$this->entity->set('type', 'non_existent');
$this->assertValidationErrors([
'type' => "The 'non_existent' plugin does not exist.",
]);
}
}

View File

@@ -0,0 +1,111 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\workflows\Unit;
use Drupal\Tests\UnitTestCase;
use Drupal\workflow_type_test\Plugin\WorkflowType\TestType;
use Drupal\workflows\State;
use Drupal\workflows\WorkflowTypeInterface;
/**
* @coversDefaultClass \Drupal\workflows\State
*
* @group workflows
*/
class StateTest extends UnitTestCase {
/**
* @covers ::__construct
* @covers ::id
* @covers ::label
* @covers ::weight
*/
public function testGetters(): void {
$state = new State(
$this->prophesize(WorkflowTypeInterface::class)->reveal(),
'draft',
'Draft',
3
);
$this->assertEquals('draft', $state->id());
$this->assertEquals('Draft', $state->label());
$this->assertEquals(3, $state->weight());
}
/**
* @covers ::canTransitionTo
*/
public function testCanTransitionTo(): void {
$workflow_type = new TestType([], '', []);
$workflow_type
->addState('draft', 'Draft')
->addState('published', 'Published')
->addTransition('publish', 'Publish', ['draft'], 'published');
$state = $workflow_type->getState('draft');
$this->assertTrue($state->canTransitionTo('published'));
$this->assertFalse($state->canTransitionTo('some_other_state'));
$workflow_type->deleteTransition('publish');
$this->assertFalse($state->canTransitionTo('published'));
}
/**
* @covers ::getTransitionTo
*/
public function testGetTransitionTo(): void {
$workflow_type = new TestType([], '', []);
$workflow_type
->addState('draft', 'Draft')
->addState('published', 'Published')
->addTransition('publish', 'Publish', ['draft'], 'published');
$state = $workflow_type->getState('draft');
$transition = $state->getTransitionTo('published');
$this->assertEquals('Publish', $transition->label());
}
/**
* @covers ::getTransitionTo
*/
public function testGetTransitionToException(): void {
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage("Can not transition to 'published' state");
$workflow_type = new TestType([], '', []);
$workflow_type->addState('draft', 'Draft');
$state = $workflow_type->getState('draft');
$state->getTransitionTo('published');
}
/**
* @covers ::getTransitions
*/
public function testGetTransitions(): void {
$workflow_type = new TestType([], '', []);
$workflow_type
->addState('draft', 'Draft')
->addState('published', 'Published')
->addState('archived', 'Archived')
->addTransition('create_new_draft', 'Create new draft', ['draft'], 'draft')
->addTransition('publish', 'Publish', ['draft'], 'published')
->addTransition('archive', 'Archive', ['published'], 'archived');
$state = $workflow_type->getState('draft');
$transitions = $state->getTransitions();
$this->assertCount(2, $transitions);
$this->assertEquals('Create new draft', $transitions['create_new_draft']->label());
$this->assertEquals('Publish', $transitions['publish']->label());
}
/**
* @covers ::labelCallback
*/
public function testLabelCallback(): void {
$workflow_type = $this->prophesize(WorkflowTypeInterface::class)->reveal();
$states = [
new State($workflow_type, 'draft', 'Draft'),
new State($workflow_type, 'published', 'Published'),
];
$this->assertEquals(['Draft', 'Published'], array_map([State::class, 'labelCallback'], $states));
}
}

View File

@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\workflows\Unit;
use Drupal\Tests\UnitTestCase;
use Drupal\workflow_type_test\Plugin\WorkflowType\TestType;
use Drupal\workflows\Transition;
use Drupal\workflows\WorkflowTypeInterface;
/**
* @coversDefaultClass \Drupal\workflows\Transition
*
* @group workflows
*/
class TransitionTest extends UnitTestCase {
/**
* @covers ::__construct
* @covers ::id
* @covers ::label
*/
public function testGetters(): void {
$state = new Transition(
$this->prophesize(WorkflowTypeInterface::class)->reveal(),
'draft_published',
'Publish',
['draft'],
'published'
);
$this->assertEquals('draft_published', $state->id());
$this->assertEquals('Publish', $state->label());
}
/**
* @covers ::from
* @covers ::to
*/
public function testFromAndTo(): void {
$workflow = new TestType([], '', []);
$workflow
->addState('draft', 'Draft')
->addState('published', 'Published')
->addTransition('publish', 'Publish', ['draft'], 'published');
$state = $workflow->getState('draft');
$transition = $state->getTransitionTo('published');
$this->assertEquals($state, $transition->from()['draft']);
$this->assertEquals($workflow->getState('published'), $transition->to());
}
}

View File

@@ -0,0 +1,151 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\workflows\Unit;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Routing\RouteMatch;
use Drupal\Core\Session\AccountInterface;
use Drupal\Tests\UnitTestCase;
use Drupal\workflows\WorkflowStateTransitionOperationsAccessCheck;
use Drupal\workflows\WorkflowInterface;
use Prophecy\Argument;
use Symfony\Component\Routing\Route;
/**
* @coversDefaultClass \Drupal\workflows\WorkflowStateTransitionOperationsAccessCheck
* @group workflows
*/
class WorkflowStateTransitionOperationsAccessCheckTest extends UnitTestCase {
/**
* Tests the access method correctly proxies to the entity access system.
*
* @covers ::access
* @dataProvider accessTestCases
*/
public function testAccess($route_requirement, $resulting_entity_access_check, $route_parameters = []): void {
$workflow_entity_access_result = AccessResult::allowed();
$workflow = $this->prophesize(WorkflowInterface::class);
$workflow->access($resulting_entity_access_check, Argument::type(AccountInterface::class), TRUE)
->shouldBeCalled()
->willReturn($workflow_entity_access_result);
$route = new Route('', [
'workflow' => NULL,
'workflow_transition' => NULL,
'workflow_state' => NULL,
], [
'_workflow_access' => $route_requirement,
]);
$route_match_params = ['workflow' => $workflow->reveal()] + $route_parameters;
$route_match = new RouteMatch(NULL, $route, $route_match_params);
$access_check = new WorkflowStateTransitionOperationsAccessCheck();
$account = $this->prophesize(AccountInterface::class);
$this->assertEquals($workflow_entity_access_result, $access_check->access($route_match, $account->reveal()));
}
/**
* Test cases for ::testAccess.
*/
public static function accessTestCases() {
return [
'Transition add' => [
'add-transition',
'add-transition',
],
'Transition update' => [
'update-transition',
'update-transition:foo-transition',
[
'workflow_transition' => 'foo-transition',
],
],
'Transition delete' => [
'delete-transition',
'delete-transition:foo-transition',
[
'workflow_transition' => 'foo-transition',
],
],
'State add' => [
'add-state',
'add-state',
],
'State update' => [
'update-state',
'update-state:bar-state',
[
'workflow_state' => 'bar-state',
],
],
'State delete' => [
'delete-state',
'delete-state:bar-state',
[
'workflow_state' => 'bar-state',
],
],
];
}
/**
* @covers ::access
*/
public function testMissingRouteParams(): void {
$workflow = $this->prophesize(WorkflowInterface::class);
$workflow->access()->shouldNotBeCalled();
$route = new Route('', [
'workflow' => NULL,
'workflow_state' => NULL,
], [
'_workflow_access' => 'update-state',
]);
$access_check = new WorkflowStateTransitionOperationsAccessCheck();
$account = $this->prophesize(AccountInterface::class);
$missing_both = new RouteMatch(NULL, $route, []);
$this->assertEquals(AccessResult::neutral(), $access_check->access($missing_both, $account->reveal()));
$missing_state = new RouteMatch(NULL, $route, [
'workflow' => $workflow->reveal(),
]);
$this->assertEquals(AccessResult::neutral(), $access_check->access($missing_state, $account->reveal()));
$missing_workflow = new RouteMatch(NULL, $route, [
'workflow_state' => 'foo',
]);
$this->assertEquals(AccessResult::neutral(), $access_check->access($missing_workflow, $account->reveal()));
}
/**
* @covers ::access
* @dataProvider invalidOperationNameTestCases
*/
public function testInvalidOperationName($operation_name): void {
$this->expectException(\Exception::class);
$this->expectExceptionMessage("Invalid _workflow_access operation '$operation_name' specified for route 'Foo Route'.");
$route = new Route('', [], [
'_workflow_access' => $operation_name,
]);
$access_check = new WorkflowStateTransitionOperationsAccessCheck();
$account = $this->prophesize(AccountInterface::class);
$access_check->access(new RouteMatch('Foo Route', $route, []), $account->reveal());
}
/**
* Test cases for ::testInvalidOperationName.
*/
public static function invalidOperationNameTestCases() {
return [
['invalid-op'],
['foo-add-transition'],
['add-transition-bar'],
];
}
}

View File

@@ -0,0 +1,712 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\workflows\Unit;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Tests\UnitTestCase;
use Drupal\workflow_type_test\Plugin\WorkflowType\TestType;
use Drupal\workflows\Entity\Workflow;
use Drupal\workflows\State;
use Drupal\workflows\Transition;
use Drupal\workflows\WorkflowTypeManager;
use Prophecy\Argument;
/**
* @coversDefaultClass \Drupal\workflows\Plugin\WorkflowTypeBase
*
* @group workflows
*/
class WorkflowTest extends UnitTestCase {
/**
* {@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('test_type', Argument::any())->willReturn(new TestType([], '', []));
$container->set('plugin.manager.workflows.type', $workflow_manager->reveal());
\Drupal::setContainer($container);
}
/**
* @covers ::addState
* @covers ::hasState
*/
public function testAddAndHasState(): void {
$workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
$this->assertFalse($workflow->getTypePlugin()->hasState('draft'));
// By default states are ordered in the order added.
$workflow->getTypePlugin()->addState('draft', 'Draft');
$this->assertTrue($workflow->getTypePlugin()->hasState('draft'));
$this->assertFalse($workflow->getTypePlugin()->hasState('published'));
$this->assertEquals(0, $workflow->getTypePlugin()->getState('draft')->weight());
// Adding a state does not set up a transition to itself.
$this->assertFalse($workflow->getTypePlugin()->hasTransitionFromStateToState('draft', 'draft'));
// New states are added with a new weight 1 more than the current highest
// weight.
$workflow->getTypePlugin()->addState('published', 'Published');
$this->assertEquals(1, $workflow->getTypePlugin()->getState('published')->weight());
}
/**
* @covers ::addState
*/
public function testAddStateException(): void {
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage("The state 'draft' already exists in workflow.");
$workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
$workflow->getTypePlugin()->addState('draft', 'Draft');
$workflow->getTypePlugin()->addState('draft', 'Draft');
}
/**
* @covers ::addState
*/
public function testAddStateInvalidIdException(): void {
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage("The state ID 'draft-draft' must contain only lowercase letters, numbers, and underscores");
$workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
$workflow->getTypePlugin()->addState('draft-draft', 'Draft');
}
/**
* @covers ::getStates
*/
public function testGetStates(): void {
$workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
// Getting states works when there are none.
$this->assertSame([], $workflow->getTypePlugin()->getStates());
$this->assertSame([], $workflow->getTypePlugin()->getStates([]));
$workflow
->getTypePlugin()
->addState('draft', 'Draft')
->addState('published', 'Published')
->addState('archived', 'Archived');
// States are stored in alphabetical key order.
$this->assertEquals([
'archived',
'draft',
'published',
], array_keys($workflow->getTypePlugin()->getConfiguration()['states']));
// Ensure we're returning state objects.
$this->assertInstanceOf(State::class, $workflow->getTypePlugin()->getStates()['draft']);
// Passing in no IDs returns all states.
$this->assertEquals(['draft', 'published', 'archived'], array_keys($workflow->getTypePlugin()->getStates()));
// The order of states is by weight.
$workflow->getTypePlugin()->setStateWeight('published', -1);
$this->assertEquals(['published', 'draft', 'archived'], array_keys($workflow->getTypePlugin()->getStates()));
// The label is also used for sorting if weights are equal.
$workflow->getTypePlugin()->setStateWeight('archived', 0);
$this->assertEquals(['published', 'archived', 'draft'], array_keys($workflow->getTypePlugin()->getStates()));
// You can limit the states returned by passing in states IDs.
$this->assertEquals(['archived', 'draft'], array_keys($workflow->getTypePlugin()->getStates(['draft', 'archived'])));
// An empty array does not load all states.
$this->assertSame([], $workflow->getTypePlugin()->getStates([]));
}
/**
* Tests numeric IDs when added to a workflow.
*/
public function testNumericIdSorting(): void {
$workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
$workflow_type = $workflow->getTypePlugin();
$workflow_type->addState('1', 'One');
$workflow_type->addState('2', 'Two');
$workflow_type->addState('3', 'ZZZ');
$workflow_type->addState('4', 'AAA');
$workflow_type->setStateWeight('1', 1);
$workflow_type->setStateWeight('2', 2);
$workflow_type->setStateWeight('3', 3);
$workflow_type->setStateWeight('4', 3);
// Ensure numeric states are correctly sorted by weight first, label second.
$this->assertEquals([1, 2, 4, 3], array_keys($workflow_type->getStates()));
}
/**
* @covers ::getStates
*/
public function testGetStatesException(): void {
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage("The state 'state_that_does_not_exist' does not exist in workflow.");
$workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
$workflow->getTypePlugin()->getStates(['state_that_does_not_exist']);
}
/**
* @covers ::getState
*/
public function testGetState(): void {
$workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
// By default states are ordered in the order added.
$workflow
->getTypePlugin()
->addState('draft', 'Draft')
->addState('published', 'Published')
->addState('archived', 'Archived')
->addTransition('create_new_draft', 'Create new draft', ['draft'], 'draft')
->addTransition('publish', 'Publish', ['draft'], 'published');
// Ensure we're returning state objects and they are set up correctly
$this->assertInstanceOf(State::class, $workflow->getTypePlugin()->getState('draft'));
$this->assertEquals('archived', $workflow->getTypePlugin()->getState('archived')->id());
$this->assertEquals('Archived', $workflow->getTypePlugin()->getState('archived')->label());
$draft = $workflow->getTypePlugin()->getState('draft');
$this->assertTrue($draft->canTransitionTo('draft'));
$this->assertTrue($draft->canTransitionTo('published'));
$this->assertFalse($draft->canTransitionTo('archived'));
$this->assertEquals('Publish', $draft->getTransitionTo('published')->label());
$this->assertEquals(0, $draft->weight());
$this->assertEquals(1, $workflow->getTypePlugin()->getState('published')->weight());
$this->assertEquals(2, $workflow->getTypePlugin()->getState('archived')->weight());
}
/**
* @covers ::getState
*/
public function testGetStateException(): void {
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage("The state 'state_that_does_not_exist' does not exist in workflow.");
$workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
$workflow->getTypePlugin()->getState('state_that_does_not_exist');
}
/**
* @covers ::setStateLabel
*/
public function testSetStateLabel(): void {
$workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
$workflow->getTypePlugin()->addState('draft', 'Draft');
$this->assertEquals('Draft', $workflow->getTypePlugin()->getState('draft')->label());
$workflow->getTypePlugin()->setStateLabel('draft', 'Unpublished');
$this->assertEquals('Unpublished', $workflow->getTypePlugin()->getState('draft')->label());
}
/**
* @covers ::setStateLabel
*/
public function testSetStateLabelException(): void {
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage("The state 'draft' does not exist in workflow.");
$workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
$workflow->getTypePlugin()->setStateLabel('draft', 'Draft');
}
/**
* @covers ::setStateWeight
*/
public function testSetStateWeight(): void {
$workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
$workflow->getTypePlugin()->addState('draft', 'Draft');
$this->assertEquals(0, $workflow->getTypePlugin()->getState('draft')->weight());
$workflow->getTypePlugin()->setStateWeight('draft', -10);
$this->assertEquals(-10, $workflow->getTypePlugin()->getState('draft')->weight());
}
/**
* @covers ::setStateWeight
*/
public function testSetStateWeightException(): void {
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage("The state 'draft' does not exist in workflow.");
$workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
$workflow->getTypePlugin()->setStateWeight('draft', 10);
}
/**
* @covers ::setStateWeight
*/
public function testSetStateWeightNonNumericException(): void {
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage("The weight 'foo' must be numeric for state 'Published'.");
$workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
$workflow->getTypePlugin()->addState('published', 'Published');
$workflow->getTypePlugin()->setStateWeight('published', 'foo');
}
/**
* @covers ::deleteState
*/
public function testDeleteState(): void {
$workflow_type = new TestType([], '', []);
$workflow_type
->addState('draft', 'Draft')
->addState('published', 'Published')
->addState('archived', 'Archived')
->addTransition('publish', 'Publish', ['draft', 'published'], 'published')
->addTransition('create_new_draft', 'Create new draft', ['draft', 'published'], 'draft')
->addTransition('archive', 'Archive', ['draft', 'published'], 'archived');
$this->assertCount(3, $workflow_type->getStates());
$this->assertCount(3, $workflow_type->getState('published')->getTransitions());
$workflow_type->deleteState('draft');
$this->assertFalse($workflow_type->hasState('draft'));
$this->assertCount(2, $workflow_type->getStates());
$this->assertCount(2, $workflow_type->getState('published')->getTransitions());
$workflow_type->deleteState('published');
$this->assertCount(0, $workflow_type->getTransitions());
}
/**
* @covers ::deleteState
*/
public function testDeleteStateException(): void {
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage("The state 'draft' does not exist in workflow.");
$workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
$workflow->getTypePlugin()->deleteState('draft');
}
/**
* @covers ::deleteState
*/
public function testDeleteOnlyStateException(): void {
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage("The state 'draft' can not be deleted from workflow as it is the only state");
$workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
$workflow->getTypePlugin()->addState('draft', 'Draft');
$workflow->getTypePlugin()->deleteState('draft');
}
/**
* @covers ::addTransition
* @covers ::hasTransition
*/
public function testAddTransition(): void {
$workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
// By default states are ordered in the order added.
$workflow
->getTypePlugin()
->addState('draft', 'Draft')
->addState('published', 'Published');
$this->assertFalse($workflow->getTypePlugin()->getState('draft')->canTransitionTo('published'));
$workflow->getTypePlugin()->addTransition('publish', 'Publish', ['draft'], 'published');
$this->assertTrue($workflow->getTypePlugin()->getState('draft')->canTransitionTo('published'));
$this->assertEquals(0, $workflow->getTypePlugin()->getTransition('publish')->weight());
$this->assertTrue($workflow->getTypePlugin()->hasTransition('publish'));
$this->assertFalse($workflow->getTypePlugin()->hasTransition('draft'));
$workflow->getTypePlugin()->addTransition('save_publish', 'Save', ['published'], 'published');
$this->assertEquals(1, $workflow->getTypePlugin()->getTransition('save_publish')->weight());
}
/**
* @covers ::addTransition
*/
public function testAddTransitionDuplicateException(): void {
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage("The transition 'publish' already exists in workflow.");
$workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
$workflow->getTypePlugin()->addState('published', 'Published');
$workflow->getTypePlugin()->addTransition('publish', 'Publish', ['published'], 'published');
$workflow->getTypePlugin()->addTransition('publish', 'Publish', ['published'], 'published');
}
/**
* @covers ::addTransition
*/
public function testAddTransitionInvalidIdException(): void {
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage("The transition ID 'publish-publish' must contain only lowercase letters, numbers, and underscores");
$workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
$workflow->getTypePlugin()->addState('published', 'Published');
$workflow->getTypePlugin()->addTransition('publish-publish', 'Publish', ['published'], 'published');
}
/**
* @covers ::addTransition
*/
public function testAddTransitionMissingFromException(): void {
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage("The state 'draft' does not exist in workflow.");
$workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
$workflow->getTypePlugin()->addState('published', 'Published');
$workflow->getTypePlugin()->addTransition('publish', 'Publish', ['draft'], 'published');
}
/**
* @covers ::addTransition
*/
public function testAddTransitionDuplicateTransitionStatesException(): void {
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage("The 'publish' transition already allows 'draft' to 'published' transitions in workflow.");
$workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
$workflow
->getTypePlugin()
->addState('draft', 'Draft')
->addState('published', 'Published');
$workflow->getTypePlugin()->addTransition('publish', 'Publish', ['draft', 'published'], 'published');
$workflow->getTypePlugin()->addTransition('draft_to_published', 'Publish a draft', ['draft'], 'published');
}
/**
* @covers ::addTransition
*/
public function testAddTransitionConsistentAfterFromCatch(): void {
$workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
$workflow->getTypePlugin()->addState('published', 'Published');
try {
$workflow->getTypePlugin()->addTransition('publish', 'Publish', ['draft'], 'published');
}
catch (\InvalidArgumentException $e) {
}
// Ensure that the workflow is not left in an inconsistent state after an
// exception is thrown from Workflow::setTransitionFromStates() whilst
// calling Workflow::addTransition().
$this->assertFalse($workflow->getTypePlugin()->hasTransition('publish'));
}
/**
* @covers ::addTransition
*/
public function testAddTransitionMissingToException(): void {
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage("The state 'published' does not exist in workflow.");
$workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
$workflow->getTypePlugin()->addState('draft', 'Draft');
$workflow->getTypePlugin()->addTransition('publish', 'Publish', ['draft'], 'published');
}
/**
* @covers ::getTransitions
* @covers ::setTransitionWeight
*/
public function testGetTransitions(): void {
$workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
// Getting transitions works when there are none.
$this->assertSame([], $workflow->getTypePlugin()->getTransitions());
$this->assertSame([], $workflow->getTypePlugin()->getTransitions([]));
// By default states are ordered in the order added.
$workflow
->getTypePlugin()
->addState('a', 'A')
->addState('b', 'B')
->addTransition('a_b', 'A to B', ['a'], 'b')
->addTransition('a_a', 'A to A', ['a'], 'a');
// Transitions are stored in alphabetical key order in configuration.
$this->assertEquals(['a_a', 'a_b'], array_keys($workflow->getTypePlugin()->getConfiguration()['transitions']));
// Ensure we're returning transition objects.
$this->assertInstanceOf(Transition::class, $workflow->getTypePlugin()->getTransitions()['a_a']);
// Passing in no IDs returns all transitions.
$this->assertEquals(['a_b', 'a_a'], array_keys($workflow->getTypePlugin()->getTransitions()));
// The order of states is by weight.
$workflow->getTypePlugin()->setTransitionWeight('a_a', -1);
$this->assertEquals(['a_a', 'a_b'], array_keys($workflow->getTypePlugin()->getTransitions()));
// If all weights are equal it will fallback to labels.
$workflow->getTypePlugin()->setTransitionWeight('a_a', 0);
$this->assertEquals(['a_a', 'a_b'], array_keys($workflow->getTypePlugin()->getTransitions()));
$workflow->getTypePlugin()->setTransitionLabel('a_b', 'A B');
$this->assertEquals(['a_b', 'a_a'], array_keys($workflow->getTypePlugin()->getTransitions()));
// You can limit the states returned by passing in states IDs.
$this->assertEquals(['a_a'], array_keys($workflow->getTypePlugin()->getTransitions(['a_a'])));
// An empty array does not load all states.
$this->assertSame([], $workflow->getTypePlugin()->getTransitions([]));
}
/**
* @covers ::getTransition
*/
public function testGetTransition(): void {
$workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
// By default states are ordered in the order added.
$workflow
->getTypePlugin()
->addState('draft', 'Draft')
->addState('published', 'Published')
->addState('archived', 'Archived')
->addTransition('create_new_draft', 'Create new draft', ['draft'], 'draft')
->addTransition('publish', 'Publish', ['draft'], 'published');
// Ensure we're returning state objects and they are set up correctly
$this->assertInstanceOf(Transition::class, $workflow->getTypePlugin()->getTransition('create_new_draft'));
$this->assertEquals('publish', $workflow->getTypePlugin()->getTransition('publish')->id());
$this->assertEquals('Publish', $workflow->getTypePlugin()->getTransition('publish')->label());
$transition = $workflow->getTypePlugin()->getTransition('publish');
$this->assertEquals($workflow->getTypePlugin()->getState('draft'), $transition->from()['draft']);
$this->assertEquals($workflow->getTypePlugin()->getState('published'), $transition->to());
}
/**
* @covers ::getTransition
*/
public function testGetTransitionException(): void {
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage("The transition 'transition_that_does_not_exist' does not exist in workflow.");
$workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
$workflow->getTypePlugin()->getTransition('transition_that_does_not_exist');
}
/**
* @covers ::getTransitionsForState
*/
public function testGetTransitionsForState(): void {
$workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
// By default states are ordered in the order added.
$workflow
->getTypePlugin()
->addState('draft', 'Draft')
->addState('published', 'Published')
->addState('archived', 'Archived')
->addTransition('create_new_draft', 'Create new draft', ['archived', 'draft'], 'draft')
->addTransition('publish', 'Publish', ['draft', 'published'], 'published')
->addTransition('archive', 'Archive', ['published'], 'archived');
$this->assertEquals(['create_new_draft', 'publish'], array_keys($workflow->getTypePlugin()->getTransitionsForState('draft')));
$this->assertEquals(['create_new_draft'], array_keys($workflow->getTypePlugin()->getTransitionsForState('draft', 'to')));
$this->assertEquals(['publish', 'archive'], array_keys($workflow->getTypePlugin()->getTransitionsForState('published')));
$this->assertEquals(['publish'], array_keys($workflow->getTypePlugin()->getTransitionsForState('published', 'to')));
$this->assertEquals(['create_new_draft'], array_keys($workflow->getTypePlugin()->getTransitionsForState('archived', 'from')));
$this->assertEquals(['archive'], array_keys($workflow->getTypePlugin()->getTransitionsForState('archived', 'to')));
}
/**
* @covers ::getTransitionFromStateToState
* @covers ::hasTransitionFromStateToState
*/
public function testGetTransitionFromStateToState(): void {
$workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
// By default states are ordered in the order added.
$workflow
->getTypePlugin()
->addState('draft', 'Draft')
->addState('published', 'Published')
->addState('archived', 'Archived')
->addTransition('create_new_draft', 'Create new draft', ['archived', 'draft'], 'draft')
->addTransition('publish', 'Publish', ['draft', 'published'], 'published')
->addTransition('archive', 'Archive', ['published'], 'archived');
$this->assertTrue($workflow->getTypePlugin()->hasTransitionFromStateToState('draft', 'published'));
$this->assertFalse($workflow->getTypePlugin()->hasTransitionFromStateToState('archived', 'archived'));
$transition = $workflow->getTypePlugin()->getTransitionFromStateToState('published', 'archived');
$this->assertEquals('Archive', $transition->label());
}
/**
* @covers ::getTransitionFromStateToState
*/
public function testGetTransitionFromStateToStateException(): void {
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage("The transition from 'archived' to 'archived' does not exist in workflow.");
$workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
// By default states are ordered in the order added.
$workflow
->getTypePlugin()
->addState('draft', 'Draft')
->addState('published', 'Published')
->addState('archived', 'Archived')
->addTransition('create_new_draft', 'Create new draft', ['archived', 'draft'], 'draft')
->addTransition('publish', 'Publish', ['draft', 'published'], 'published')
->addTransition('archive', 'Archive', ['published'], 'archived');
$workflow->getTypePlugin()->getTransitionFromStateToState('archived', 'archived');
}
/**
* @covers ::setTransitionLabel
*/
public function testSetTransitionLabel(): void {
$workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
$workflow
->getTypePlugin()
->addState('draft', 'Draft')
->addState('published', 'Published')
->addTransition('publish', 'Publish', ['draft'], 'published');
$this->assertEquals('Publish', $workflow->getTypePlugin()->getTransition('publish')->label());
$workflow->getTypePlugin()->setTransitionLabel('publish', 'Publish!');
$this->assertEquals('Publish!', $workflow->getTypePlugin()->getTransition('publish')->label());
}
/**
* @covers ::setTransitionLabel
*/
public function testSetTransitionLabelException(): void {
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage("The transition 'draft-published' does not exist in workflow.");
$workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
$workflow->getTypePlugin()->addState('published', 'Published');
$workflow->getTypePlugin()->setTransitionLabel('draft-published', 'Publish');
}
/**
* @covers ::setTransitionWeight
*/
public function testSetTransitionWeight(): void {
$workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
$workflow
->getTypePlugin()
->addState('draft', 'Draft')
->addState('published', 'Published')
->addTransition('publish', 'Publish', ['draft'], 'published');
$this->assertEquals(0, $workflow->getTypePlugin()->getTransition('publish')->weight());
$workflow->getTypePlugin()->setTransitionWeight('publish', 10);
$this->assertEquals(10, $workflow->getTypePlugin()->getTransition('publish')->weight());
}
/**
* @covers ::setTransitionWeight
*/
public function testSetTransitionWeightException(): void {
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage("The transition 'draft-published' does not exist in workflow.");
$workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
$workflow->getTypePlugin()->addState('published', 'Published');
$workflow->getTypePlugin()->setTransitionWeight('draft-published', 10);
}
/**
* @covers ::setTransitionWeight
*/
public function testSetTransitionWeightNonNumericException(): void {
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage("The weight 'foo' must be numeric for transition 'Publish'.");
$workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
$workflow->getTypePlugin()->addState('published', 'Published');
$workflow->getTypePlugin()->addTransition('publish', 'Publish', [], 'published');
$workflow->getTypePlugin()->setTransitionWeight('publish', 'foo');
}
/**
* @covers ::setTransitionFromStates
*/
public function testSetTransitionFromStates(): void {
$workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
$workflow
->getTypePlugin()
->addState('draft', 'Draft')
->addState('published', 'Published')
->addState('archived', 'Archived')
->addTransition('test', 'Test', ['draft'], 'draft');
$this->assertTrue($workflow->getTypePlugin()->hasTransitionFromStateToState('draft', 'draft'));
$this->assertFalse($workflow->getTypePlugin()->hasTransitionFromStateToState('published', 'draft'));
$this->assertFalse($workflow->getTypePlugin()->hasTransitionFromStateToState('archived', 'draft'));
$workflow->getTypePlugin()->setTransitionFromStates('test', ['draft', 'published', 'archived']);
$this->assertTrue($workflow->getTypePlugin()->hasTransitionFromStateToState('draft', 'draft'));
$this->assertTrue($workflow->getTypePlugin()->hasTransitionFromStateToState('published', 'draft'));
$this->assertTrue($workflow->getTypePlugin()->hasTransitionFromStateToState('archived', 'draft'));
$workflow->getTypePlugin()->setTransitionFromStates('test', ['published', 'archived']);
$this->assertFalse($workflow->getTypePlugin()->hasTransitionFromStateToState('draft', 'draft'));
$this->assertTrue($workflow->getTypePlugin()->hasTransitionFromStateToState('published', 'draft'));
$this->assertTrue($workflow->getTypePlugin()->hasTransitionFromStateToState('archived', 'draft'));
}
/**
* @covers ::setTransitionFromStates
*/
public function testSetTransitionFromStatesMissingTransition(): void {
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage("The transition 'test' does not exist in workflow.");
$workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
$workflow
->getTypePlugin()
->addState('draft', 'Draft')
->addState('published', 'Published')
->addState('archived', 'Archived')
->addTransition('create_new_draft', 'Create new draft', ['draft'], 'draft');
$workflow->getTypePlugin()->setTransitionFromStates('test', ['draft', 'published', 'archived']);
}
/**
* @covers ::setTransitionFromStates
*/
public function testSetTransitionFromStatesMissingState(): void {
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage("The state 'published' does not exist in workflow.");
$workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
$workflow
->getTypePlugin()
->addState('draft', 'Draft')
->addState('archived', 'Archived')
->addTransition('create_new_draft', 'Create new draft', ['draft'], 'draft');
$workflow->getTypePlugin()->setTransitionFromStates('create_new_draft', ['draft', 'published', 'archived']);
}
/**
* @covers ::setTransitionFromStates
*/
public function testSetTransitionFromStatesAlreadyExists(): void {
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage("The 'create_new_draft' transition already allows 'draft' to 'draft' transitions in workflow.");
$workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
$workflow
->getTypePlugin()
->addState('draft', 'Draft')
->addState('archived', 'Archived')
->addState('needs_review', 'Needs Review')
->addTransition('create_new_draft', 'Create new draft', ['draft'], 'draft')
->addTransition('needs_review', 'Needs review', ['needs_review'], 'draft');
$workflow->getTypePlugin()->setTransitionFromStates('needs_review', ['draft']);
}
/**
* @covers ::deleteTransition
*/
public function testDeleteTransition(): void {
$workflow_type = new TestType([], '', []);
$workflow_type
->addState('draft', 'Draft')
->addState('published', 'Published')
->addTransition('create_new_draft', 'Create new draft', ['draft'], 'draft')
->addTransition('publish', 'Publish', ['draft'], 'published');
$this->assertTrue($workflow_type->getState('draft')->canTransitionTo('published'));
$workflow_type->deleteTransition('publish');
$this->assertFalse($workflow_type->getState('draft')->canTransitionTo('published'));
$this->assertTrue($workflow_type->getState('draft')->canTransitionTo('draft'));
}
/**
* @covers ::deleteTransition
*/
public function testDeleteTransitionException(): void {
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage("The transition 'draft-published' does not exist in workflow.");
$workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
$workflow->getTypePlugin()->addState('published', 'Published');
$workflow->getTypePlugin()->deleteTransition('draft-published');
}
/**
* @covers \Drupal\workflows\Entity\Workflow::status
*/
public function testStatus(): void {
$workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
$this->assertFalse($workflow->status());
$workflow->getTypePlugin()->addState('published', 'Published');
$this->assertTrue($workflow->status());
}
}