diff --git a/core/composer.json b/core/composer.json index 550c781..1c7fc8a 100644 --- a/core/composer.json +++ b/core/composer.json @@ -109,6 +109,7 @@ "drupal/migrate_drupal_ui": "self.version", "drupal/node": "self.version", "drupal/options": "self.version", + "drupal/outside_in": "self.version", "drupal/page_cache": "self.version", "drupal/path": "self.version", "drupal/quickedit": "self.version", diff --git a/core/modules/outside_in/css/offcanvas.css b/core/modules/outside_in/css/offcanvas.css new file mode 100644 index 0000000..c4d387c --- /dev/null +++ b/core/modules/outside_in/css/offcanvas.css @@ -0,0 +1,43 @@ +#offcanvas { + background: #3c3c3c; + color: #fff; + + box-sizing: border-box; + z-index: 1000; + + position: fixed; + height: 100%; + overflow-y: auto; + + border-left: 1px solid #000; + box-shadow: -2px 3px 1px 1px rgba(0, 0, 0, 0.3333); + + display: inline-block; + + width: 25%; +} + +#offcanvas span.offcanvasClose { + float: right; + margin: 15px 15px 0 0; +} + +#offcanvas h1 { + margin: 0 0 15px 0; + padding: 15px; + color: #fff; + border-bottom: 1px solid #fff; +} + +#offcanvas .content { + padding: 15px; +} + +#canvas-tray.offCanvasDisplayInProgress { + position: fixed; + display: inline-block; +} + +#canvas-tray.offCanvasDisplayed { + display: inline-block; +} diff --git a/core/modules/outside_in/css/outside_in.base.css b/core/modules/outside_in/css/outside_in.base.css new file mode 100644 index 0000000..ddf3b38 --- /dev/null +++ b/core/modules/outside_in/css/outside_in.base.css @@ -0,0 +1,184 @@ +/** + * @file + * Resets for Outside-In module. + */ + +/** + * Resets + * inside the wrapper override font styling the page provides, this may look a bit extreme + * but any module may place any new form element in this tray therefore CSS from any + * front-end-theme needs to be overridden + */ +#offcanvas abbr, +#offcanvas acronym, +#offcanvas address, +#offcanvas article, +#offcanvas aside, +#offcanvas audio, +#offcanvas b, +#offcanvas caption, +#offcanvas center, +#offcanvas cite, +#offcanvas code, +#offcanvas dd, +#offcanvas del, +#offcanvas details, +#offcanvas dfn, +#offcanvas dialog, +#offcanvas div, +#offcanvas dl, +#offcanvas dt, +#offcanvas em, +#offcanvas embed, +#offcanvas fieldset, +#offcanvas figcaption, +#offcanvas figure, +#offcanvas font, +#offcanvas footer, +#offcanvas form, +#offcanvas h1, +#offcanvas h2, +#offcanvas h3, +#offcanvas h4, +#offcanvas h5, +#offcanvas h6, +#offcanvas header, +#offcanvas hgroup, +#offcanvas i, +#offcanvas img, +#offcanvas ins, +#offcanvas kbd, +#offcanvas label, +#offcanvas legend, +#offcanvas li, +#offcanvas mark, +#offcanvas menu, +#offcanvas meter, +#offcanvas nav, +#offcanvas object, +#offcanvas ol, +#offcanvas output, +#offcanvas p, +#offcanvas pre, +#offcanvas progress, +#offcanvas q, +#offcanvas rp, +#offcanvas rt, +#offcanvas ruby, +#offcanvas s, +#offcanvas samp, +#offcanvas section, +#offcanvas small, +#offcanvas span, +#offcanvas strike, +#offcanvas strong, +#offcanvas sub, +#offcanvas summary, +#offcanvas sup, +#offcanvas table, +#offcanvas tbody, +#offcanvas td, +#offcanvas tfoot, +#offcanvas th, +#offcanvas thead, +#offcanvas time, +#offcanvas tr, +#offcanvas tt, +#offcanvas u, +#offcanvas ul, +#offcanvas var, +#offcanvas video { + font-family: 'Lucida Grande','Lucida Sans Unicode','Liberation Sans',sans-serif; + color: #eee; + font-size: 13px; + font-weight: normal; + font-style: normal; + line-height: 1.5em; + text-transform: none; + text-align: left; /* LTR */ + text-indent: 0; + padding:0; +} +/* reset font family rules from formalize to match Druapl (seven theme) */ +#offcanvas ::-webkit-validation-bubble-message, +#offcanvas optgroup, +#offcanvas .ie6_input, +#offcanvas * html textarea, +#offcanvas * html select, +#offcanvas textarea, +#offcanvas select, +#offcanvas button, +#offcanvas a.button, +#offcanvas input[type="date"], +#offcanvas input[type="datetime"], +#offcanvas input[type="datetime-local"], +#offcanvas input[type="email"], +#offcanvas input[type="month"], +#offcanvas input[type="number"], +#offcanvas input[type="password"], +#offcanvas input[type="search"], +#offcanvas input[type="tel"], +#offcanvas input[type="text"], +#offcanvas input[type="time"], +#offcanvas input[type="url"], +#offcanvas input[type="week"], +#offcanvas input[type="reset"], +#offcanvas input[type="submit"], +#offcanvas input[type="button"] { + font-family: 'Lucida Grande','Lucida Sans Unicode','Liberation Sans',sans-serif; +} +/* reset the link color to one that stands out on dark background */ +#offcanvas a { + color: #33aaff; + text-decoration: none; +} +/* reset to sensible vertical rhythm */ +#offcanvas .form-item, +#offcanvas details, +#offcanvas button, +.form-type-checkbox, +.form-type-radio { + margin-top: 0; + margin-bottom: 1.25em; + padding-right: .5em; /* LTR */ + display: inline-block; +} +/* re-set defaults for fieldsets */ +#offcanvas .fieldset-wrapper { + margin: 0; + padding: 0; + border: 0; +} +/* override formalize label styling */ +#offcanvas label { + font-size: 85%; + font-weight: bold; + line-height: 1.5em; +} +/* override formalize button styling */ +#offcanvas button, +#offcanvas .button, +#offcanvas input[type="button"], +#offcanvas input[type="reset"] { + font-weight: normal; + text-shadow: none; + width: auto; + max-width: 50%; + color: #fff; + border-radius: 30px; + border: none; + background-color: #777; + background-image: none; + background-image: none; +} + /* remove browser and formalize focus effects for dark background */ +#offcanvas input:focus, +#offcanvas button:focus, +#offcanvas a.button:focus, +#offcanvas select:focus, +#offcanvas textarea:focus { + -webkit-box-shadow: none; + -moz-box-shadow: none; + box-shadow: none; + outline: none; +} diff --git a/core/modules/outside_in/css/outside_in.module.css b/core/modules/outside_in/css/outside_in.module.css new file mode 100644 index 0000000..fa659a7 --- /dev/null +++ b/core/modules/outside_in/css/outside_in.module.css @@ -0,0 +1,213 @@ +/** + * @file + * Styling for Outside-In module. + */ + +/* style the offcanvas container */ +#offcanvas { + box-sizing: border-box; + position: fixed; + height: 100%; + overflow-y: auto; + box-shadow: -2px 3px 1px 1px rgba(0, 0, 0, 0.3333); /* LTR */ + display: inline-block; + width: 25%; + -webkit-transform: translate(100%, 0); + -moz-transform: translate(100%, 0); + -o-transform: translate(100%, 0); + -ms-transform: translate(100%, 0); + transform: translate(100%, 0); + transition: all 0.75s; + -webkit-transition: all 0.75s; + -moz-transition: all 0.75s; + z-index: 1000; +} +[dir="rtl"] #offcanvas { + right: auto; + left: 0; +} +#canvas-tray-wrapper #canvas-tray { + width: 100%; + display: inline-block; + transition: all 0.75s; + -webkit-transition: all 0.75s; + -moz-transition: all 0.75s; +} +#canvas-tray-wrapper.js-tray-open #canvas-tray { + width: 75%; +} +#canvas-tray-wrapper.js-tray-open #offcanvas { + -webkit-transform: translate(0, 0); + -moz-transform: translate(0, 0); + -o-transform: translate(0, 0); + -ms-transform: translate(0, 0); + transform: translate(0, 0); +} +/* button that closes the offcanvas tray */ +#offcanvas > button.offcanvasClose, +#offcanvas > button.offcanvasClose:hover { /* todo: add hover state */ + position: absolute; + width: 30px; + right: 0; /* LTR */ + top: 1em; + border: 0; + border-radius: 0; + background: url(/core/misc/icons/ffffff/ex.svg) 0 0 no-repeat; + color: transparent; + cursor: pointer; +} +[dir="rtl"] #offcanvas > span.offcanvasClose { + right: auto; + left: 0; +} +#offcanvas > .content { + height: 100%; + padding: 15px; +} +#canvas-tray.offCanvasDisplayInProgress { + position: fixed; + display: inline-block; +} +#canvas-tray.offCanvasDisplayed { + display: inline-block; +} + +/** + * Buttons. + */ + +/* style for primary button */ +#offcanvas .button--primary { + width: auto; + max-width: 50%; + border-radius: 30px; + border: none; + background-image: -webkit-linear-gradient(top,#007bc6,#0071b8); + background-image: linear-gradient(to bottom,#007bc6,#0071b8); +} +#offcanvas .button--primary:hover, +#offcanvas .button--primary:active { + background-color: inherit; +} +/* Style for delete button */ +#offcanvas .button--danger { + width: auto; + max-width: 50%; + border-radius: none; + border: none; + background: transparent; +} +/* special edit link to flow inline with form items that link to more configuration */ +#offcanvas .edit-link { /* todo add markup and class to template */ + display: inline-block; + font-size: 85%; + font-weight: bold; +} + +/** + * Inputs. + */ +#offcanvas textarea, +#offcanvas select, +#offcanvas input[type="date"], +#offcanvas input[type="datetime"], +#offcanvas input[type="datetime-local"], +#offcanvas input[type="email"], +#offcanvas input[type="month"], +#offcanvas input[type="number"], +#offcanvas input[type="password"], +#offcanvas input[type="search"], +#offcanvas input[type="tel"], +#offcanvas input[type="text"], +#offcanvas input[type="time"], +#offcanvas input[type="url"], +#offcanvas input[type="week"], +#offcanvas input[type="range"] { + display: block; + width: 100%; +} +/* reduce the size of descriptions and other elements for scanability */ +#offcanvas .description, +#offcanvas .machine-name-label, +#offcanvas .machine-name-value { + font-size: 85%; + font-weight: normal; + padding-top: .25em; +} + +/* add a background to checkboxes, radois for contrast */ +/* +#offcanvas .radio-wrapper { + display: inline-block; + padding: 1px; + border-radius: 20px; +} +#offcanvas .checkbox-wrapper { + display: inline-block; + padding: 1px; + border-radius: 3px; +} +*/ + +/** + * Fieldsets. + */ +#offcanvas fieldset { + border: 0px solid transparent; + margin-left:-1em; /* push background to the left edge of the parent */ + margin-right:-1em; /* push background to the right edge of the parent */ + padding-top: 2.5em; + padding-left: 1em; + padding-bottom: 1em; + position: relative; +} +#offcanvas fieldset .form-item { + display: block; +} +/* style fieldset legend similar to Drupal (seven theme) */ +#offcanvas span.fieldset-legend { + text-transform: uppercase; + letter-spacing: 0.08em; + font-size: 85%; + position: absolute; + top: 10px; +} + +/** + * Admin label. + */ +#offcanvas .form-item-settings-admin-label { + font-size: 1.25em; + width: 100%; + padding: 1em; + padding-top: 0; + margin-top: 0; + margin-left: -1em; + margin-right: -1em; + margin-bottom: 1em; + border-bottom-width: 1px; + border-bottom-style: solid; +} +#offcanvas .form-item-settings-admin-label label { + font-size: 10px; + display: block; + margin-bottom: 0; +} + +#offcanvas span.field-suffix small { /* don't know why it's there. todo: figure out why it's rendering empty and taking space */ + display: none; +} + +/** + * Form actions. + */ +#offcanvas .form-actions { + padding: 1em; + margin-left: -1em; + margin-right: -1em; + border-bottom-width: 1px; + border-bottom-style: solid; +} + + + diff --git a/core/modules/outside_in/css/outside_in.theme.css b/core/modules/outside_in/css/outside_in.theme.css new file mode 100644 index 0000000..acd176d --- /dev/null +++ b/core/modules/outside_in/css/outside_in.theme.css @@ -0,0 +1,128 @@ +/** + * @file + * Visual styling for Outside-In module. + * + * Since visual styling here differs from styling elsewhere in core, + * font and color styles are put here so they can be more easily changed. + * + */ + +/* style the offcanvas container */ +#offcanvas { + color: #eee; + border-left: 1px solid #000; + background: #444; + font-family: 'Lucida Grande','Lucida Sans Unicode','Liberation Sans',sans-serif; +} +#offcanvas .content { + color: #eee; +} +#offcanvas h1 { + margin: 0 0 15px 0; + padding: 15px; + color: #eee; + border-bottom: 1px solid #777; +} + +/** + * Buttons. + */ +#offcanvas button:hover, +#offcanvas button:active, +#offcanvas .button:hover, +#offcanvas .button:active { + background-color: inherit; + background-image: inherit; + background-image: inherit; +} +/* style for primary button */ +#offcanvas .button--primary { + background-image: -webkit-linear-gradient(top,#007bc6,#0071b8); + background-image: linear-gradient(to bottom,#007bc6,#0071b8); + font-weight: bold; + text-shadow: none; + color: #fff; +} +#offcanvas .button--primary:hover, +#offcanvas .button--primary:active { + background-color: inherit; + background-image: -webkit-linear-gradient(top,#007bc6,#0071b8); /* todo: make the hover gradient different from normal */ + background-image: linear-gradient(to bottom,#007bc6,#0071b8); /* todo: make the hover gradient different from normal */ +} +/* Style for delete button */ +#offcanvas .button--danger { + font-weight: bold; + text-shadow: none; + color: #ff3333; +} + +/** + * Inputs. + */ +#offcanvas textarea, +#offcanvas select, +#offcanvas input[type="date"], +#offcanvas input[type="datetime"], +#offcanvas input[type="datetime-local"], +#offcanvas input[type="email"], +#offcanvas input[type="month"], +#offcanvas input[type="number"], +#offcanvas input[type="password"], +#offcanvas input[type="search"], +#offcanvas input[type="tel"], +#offcanvas input[type="text"], +#offcanvas input[type="time"], +#offcanvas input[type="url"], +#offcanvas input[type="week"], +#offcanvas input[type="range"] { + background-color: #ddd; /* darken inputs to reduce contrast on dark background */ + border: 1px solid #333; +} +/* add focus effect to fields for dark background */ +#offcanvas textarea:focus, +#offcanvas select:focus, +#offcanvas input:focus { + background-color: #fff; +} +/* reduce the size of descriptions and other elements for scanability */ +#offcanvas .description, +#offcanvas .machine-name-label, +#offcanvas .machine-name-value { + font-family: 'Lucida Grande','Lucida Sans Unicode','Liberation Sans',sans-serif; + color: #bbb; +} +/* add a background to radio buttons for contrast */ +/* +#offcanvas .radio-wrapper { + background-color: #000; +} +#offcanvas .checkbox-wrapper { + background-color: #000; +} +*/ + +/** + * Fieldsets. + */ +#offcanvas fieldset { + background-color: #333; +} + +/** + * Admin label. + */ +#offcanvas .form-item-settings-admin-label { + border-bottom-color: #777; +} +#offcanvas .form-item-settings-admin-label label { + color: #bbb; +} + +/** + * Form actions. + */ +#offcanvas .form-actions { + border-bottom-color: #777; +} + + diff --git a/core/modules/outside_in/js/offcanvas.js b/core/modules/outside_in/js/offcanvas.js new file mode 100644 index 0000000..3f4a958 --- /dev/null +++ b/core/modules/outside_in/js/offcanvas.js @@ -0,0 +1,122 @@ +/** + * @file + * Drupal's off canvas library. + */ + +(function ($, Drupal) { + 'use strict'; + + /** + * Create a wrapper container for the off canvas element. + * @param {number} pageWidth + * The width of #page-wrapper. + * @return {object} + * jQuery object that is the off canvas wrapper element. + */ + var createOffCanvasWrapper = function (pageWidth) { + return $('
', { + 'id': 'offcanvas', + 'role': 'region', + 'aria-labelledby': 'offcanvas-header' + }); + }; + + /** + * Create the title element for the off canvas element. + * @param {string} title + * The title string. + * @return {object} + * jQuery object that is the off canvas title element. + */ + var createTitle = function (title) { + return $('', {text: title, id: 'offcanvas-header'}); + }; + + /** + * Create the actual off canvas content. + * @param {string} data + * This is fully rendered html from Drupal. + * @return {object} + * jQuery object that is the off canvas content element. + */ + var createOffCanvasContent = function (data) { + return $('', {class: 'content', html: data}); + }; + + /** + * Create the off canvas close element. + * @param {object} offCanvasWrapper + * The jQuery off canvas wrapper element + * @param {object} pageWrapper + * The jQuery off page wrapper element + * @return {object} + * jQuery object that is the off canvas close element. + */ + var createOffCanvasClose = function (offCanvasWrapper, pageWrapper) { + return $('', { + 'class': 'offcanvasClose', + 'aria-label': Drupal.t('Close configuration tray.'), + 'text': 'x' + }).click(function () { + pageWrapper + .removeClass('js-tray-open'); + offCanvasWrapper.one('webkitTransitionEnd otransitionend oTransitionEnd msTransitionEnd transitionend', + function (e) { + Drupal.offCanvas.visible = false; + offCanvasWrapper.remove(); + Drupal.announce(Drupal.t('Configuration tray closed.')); + } + ); + }); + }; + + + /** + * Command to open an off canvas element. + * + * @param {Drupal.Ajax} ajax + * The Drupal Ajax object. + * @param {object} response + * Object holding the server response. + * @param {number} [status] + * The HTTP status code. + * + * @return {bool} + * Returns false. + */ + Drupal.AjaxCommands.prototype.openOffCanvas = function (ajax, response, status) { + // Set animation duration and get #page-wrapper width. + var $pageWrapper = $('#canvas-tray-wrapper'); + var pageWidth = $pageWrapper.width(); + + // Set the initial state of the off canvas element. + // If the state has been set previously, use it. + Drupal.offCanvas = { + visible: Drupal.offCanvas ? Drupal.offCanvas.visible : false + }; + + // Construct off canvas wrapper + var $offcanvasWrapper = createOffCanvasWrapper(pageWidth); + + // Construct off canvas internal elements. + var $offcanvasClose = createOffCanvasClose($offcanvasWrapper, $pageWrapper); + var $title = createTitle(response.dialogOptions.title); + var $offcanvasContent = createOffCanvasContent(response.data); + + // Put everything together. + $offcanvasWrapper.append([$offcanvasClose, $title, $offcanvasContent]); + + // Only add off canvas elements if we have none visible. + if (!Drupal.offCanvas.visible) { + // Append off canvas wrapper to the 'page' + $pageWrapper.append($offcanvasWrapper); + + Drupal.offCanvas.visible = true; + $pageWrapper + .addClass('js-tray-open'); + } + + return false; + }; + +})(jQuery, Drupal); diff --git a/core/modules/outside_in/outside_in.info.yml b/core/modules/outside_in/outside_in.info.yml new file mode 100644 index 0000000..ecf9c3d --- /dev/null +++ b/core/modules/outside_in/outside_in.info.yml @@ -0,0 +1,9 @@ +name: 'Outside In' +type: module +description: 'Provides the ability to access useful configuration from the Drupal front-end.' +package: Core (Experimental) +version: VERSION +core: 8.x +dependencies: + - block + - toolbar diff --git a/core/modules/outside_in/outside_in.libraries.yml b/core/modules/outside_in/outside_in.libraries.yml new file mode 100644 index 0000000..d39acd9 --- /dev/null +++ b/core/modules/outside_in/outside_in.libraries.yml @@ -0,0 +1,16 @@ +drupal.off_canvas: + version: VERSION + js: + js/offcanvas.js: {} + css: + component: + css/outside_in.base.css: {} + css/outside_in.module.css: {} + css/outside_in.theme.css: {} + dependencies: + - core/jquery + - core/drupal + - core/drupal.ajax + - core/drupal.announce + - core/drupal.dialog + - core/drupal.dialog.ajax diff --git a/core/modules/outside_in/outside_in.links.contextual.yml b/core/modules/outside_in/outside_in.links.contextual.yml new file mode 100644 index 0000000..ee83136 --- /dev/null +++ b/core/modules/outside_in/outside_in.links.contextual.yml @@ -0,0 +1,4 @@ +outside_in.block_configure: + title: 'Quick Edit' + route_name: 'entity.block.edit_form' + group: 'block' diff --git a/core/modules/outside_in/outside_in.module b/core/modules/outside_in/outside_in.module new file mode 100644 index 0000000..25dca89 --- /dev/null +++ b/core/modules/outside_in/outside_in.module @@ -0,0 +1,83 @@ +' . t('About') . ''; + // @todo Update help text. + $output .= '' . t('The Outside In module is something that we should have help for. For more information, see the online documentation for the Outside In module.', [':outside-in-documentation' => 'https://www.drupal.org/documentation/modules/outside_in']) . '
'; + return $output; + } +} + +/** + * Implements hook_contextual_links_view_alter(). + * + * Change Configure Blocks into offcanvas links. + */ +function outside_in_contextual_links_view_alter(&$element, $items) { + if (isset($element['#links']['outside-inblock-configure'])) { + $element['#links']['outside-inblock-configure']['attributes'] = [ + 'class' => ['use-ajax'], + 'data-dialog-type' => 'offcanvas', + ]; + + $element['#attached'] = [ + 'library' => [ + 'outside_in/drupal.off_canvas', + ], + ]; + } +} + +/** + * Implements hook_page_top(). + * + * Opens tag consistant wrapping to html.html.twig for all themes. + */ +function outside_in_page_top(array &$page_top) { + // TODO: rename 'outside_in' longterm. + $page_top['outside_in'] = [ + '#markup' => 'Text!
', array( + 'url' => 'example', + ), + )) + ->setMethods(array('getRenderedContent')) + ->getMock(); + + // This method calls the render service, which isn't available. We want it + // to do nothing so we mock it to return a known value. + $command->expects($this->once()) + ->method('getRenderedContent') + ->willReturn('rendered content'); + + $expected = array( + 'command' => 'openOffCanvas', + 'selector' => '#drupal-offcanvas', + 'settings' => NULL, + 'data' => 'rendered content', + 'dialogOptions' => array( + 'url' => 'example', + 'title' => 'Title', + 'modal' => FALSE, + ), + ); + $this->assertEquals($expected, $command->render()); + } + +} diff --git a/core/modules/outside_in/tests/src/Unit/OutsideInBlockManagerTest.php b/core/modules/outside_in/tests/src/Unit/OutsideInBlockManagerTest.php new file mode 100644 index 0000000..a3d7b58 --- /dev/null +++ b/core/modules/outside_in/tests/src/Unit/OutsideInBlockManagerTest.php @@ -0,0 +1,138 @@ +classResolver = $this->prophesize(ClassResolverInterface::class); + $this->manager = new OutsideInBlockManager($this->classResolver->reveal()); + } + + /** + * @covers ::getFormObject + */ + public function testGetFormObject() { + $plugin_form = $this->prophesize(PluginFormInterface::class); + $expected = $plugin_form->reveal(); + + $this->classResolver->getInstanceFromDefinition(get_class($expected))->willReturn($expected); + + $plugin = $this->prophesize(PluginInspectionInterface::class); + $plugin->getPluginDefinition()->willReturn([ + 'form' => [ + 'standard_class' => get_class($expected), + ], + ]); + + $form_object = $this->manager->getFormObject($plugin->reveal(), 'standard_class'); + $this->assertSame($expected, $form_object); + } + + /** + * @covers ::getFormObject + */ + public function testGetFormObjectUsingPlugin() { + $this->classResolver->getInstanceFromDefinition(Argument::cetera())->shouldNotBeCalled(); + + $plugin = $this->prophesize(PluginInspectionInterface::class)->willImplement(PluginFormInterface::class); + $plugin->getPluginDefinition()->willReturn([ + 'form' => [ + 'default' => get_class($plugin->reveal()), + ], + ]); + + $form_object = $this->manager->getFormObject($plugin->reveal(), 'default'); + $this->assertSame($plugin->reveal(), $form_object); + } + + /** + * @covers ::getFormObject + */ + public function testGetFormObjectOperationAware() { + $plugin_form = $this->prophesize(PluginFormInterface::class)->willImplement(OperationAwareFormInterface::class); + $plugin_form->setOperation('operation_aware')->shouldBeCalled(); + + $expected = $plugin_form->reveal(); + + $this->classResolver->getInstanceFromDefinition(get_class($expected))->willReturn($expected); + + $plugin = $this->prophesize(PluginInspectionInterface::class); + $plugin->getPluginDefinition()->willReturn([ + 'form' => [ + 'operation_aware' => get_class($expected), + ], + ]); + + $form_object = $this->manager->getFormObject($plugin->reveal(), 'operation_aware'); + $this->assertSame($expected, $form_object); + } + + /** + * @covers ::getFormObject + */ + public function testGetFormObjectDefinitionException() { + $this->setExpectedException(InvalidPluginDefinitionException::class, 'The "the_plugin_id" plugin did not specify a "anything" form class'); + + $plugin = $this->prophesize(PluginInspectionInterface::class); + $plugin->getPluginId()->willReturn('the_plugin_id'); + $plugin->getPluginDefinition()->willReturn([]); + + $form_object = $this->manager->getFormObject($plugin->reveal(), 'anything'); + $this->assertSame(NULL, $form_object); + } + + /** + * @covers ::getFormObject + */ + public function testGetFormObjectInvalidException() { + $this->setExpectedException(InvalidPluginDefinitionException::class, 'The "the_plugin_id" plugin did not specify a valid "invalid" form class, must implement \Drupal\Core\Plugin\PluginFormInterface'); + + $expected = new \stdClass(); + $this->classResolver->getInstanceFromDefinition(get_class($expected))->willReturn($expected); + + $plugin = $this->prophesize(PluginInspectionInterface::class); + $plugin->getPluginId()->willReturn('the_plugin_id'); + $plugin->getPluginDefinition()->willReturn([ + 'form' => [ + 'invalid' => get_class($expected), + ], + ]); + + $form_object = $this->manager->getFormObject($plugin->reveal(), 'invalid'); + $this->assertSame(NULL, $form_object); + } + +}