diff --git a/app/code/Magento/Catalog/view/frontend/layout/catalog_product_view.xml b/app/code/Magento/Catalog/view/frontend/layout/catalog_product_view.xml index 991d9a36d18e04c7e8b17744dd3b2d32981fdc18..2f8feb52db5ffc071055bebfb436b449331dbb1c 100644 --- a/app/code/Magento/Catalog/view/frontend/layout/catalog_product_view.xml +++ b/app/code/Magento/Catalog/view/frontend/layout/catalog_product_view.xml @@ -101,7 +101,33 @@ </block> </container> <container name="product.info.media" htmlTag="div" htmlClass="product media" after="product.info.main"> + <block class="Magento\Framework\View\Element\Template" name="skip_gallery_after.target" before="skip_gallery_before.wrapper" template="Magento_Theme::html/skiptarget.phtml"> + <arguments> + <argument name="target_id" xsi:type="string">gallery-prev-area</argument> + </arguments> + </block> + <container name="skip_gallery_before.wrapper" htmlTag="div" htmlClass="action-skip-wrapper"> + <block class="Magento\Framework\View\Element\Template" before="product.info.media.image" name="skip_gallery_before" template="Magento_Theme::html/skip.phtml"> + <arguments> + <argument name="target" xsi:type="string">gallery-next-area</argument> + <argument name="label" translate="true" xsi:type="string">Skip to the end of the images gallery</argument> + </arguments> + </block> + </container> <block class="Magento\Catalog\Block\Product\View\Gallery" name="product.info.media.image" template="product/view/gallery.phtml"/> + <container name="skip_gallery_after.wrapper" htmlTag="div" htmlClass="action-skip-wrapper"> + <block class="Magento\Framework\View\Element\Template" after="product.info.media.image" name="skip_gallery_after" template="Magento_Theme::html/skip.phtml"> + <arguments> + <argument name="target" xsi:type="string">gallery-prev-area</argument> + <argument name="label" translate="true" xsi:type="string">Skip to the beginning of the images gallery</argument> + </arguments> + </block> + </container> + <block class="Magento\Framework\View\Element\Template" name="skip_gallery_before.target" after="skip_gallery_after.wrapper" template="Magento_Theme::html/skiptarget.phtml"> + <arguments> + <argument name="target_id" xsi:type="string">gallery-next-area</argument> + </arguments> + </block> </container> <block class="Magento\Catalog\Block\Product\View\Description" name="product.info.details" template="product/view/details.phtml" after="product.info.media"> <block class="Magento\Catalog\Block\Product\View\Description" name="product.info.description" template="product/view/attribute.phtml" group="detailed_info"> diff --git a/app/code/Magento/CatalogSearch/view/frontend/templates/advanced/result.phtml b/app/code/Magento/CatalogSearch/view/frontend/templates/advanced/result.phtml index b63acf736f99f4102c9bdd89568959cd1e36ba7f..3238f68401f178530ea3dc684d518bca619ab847 100644 --- a/app/code/Magento/CatalogSearch/view/frontend/templates/advanced/result.phtml +++ b/app/code/Magento/CatalogSearch/view/frontend/templates/advanced/result.phtml @@ -21,7 +21,7 @@ <?php endif; ?> </div> <?php else: ?> - <div class="message error"> + <div role="alert" class="message error"> <div> <?php /* @escapeNotVerified */ echo __('We can\'t find any items matching these search criteria.');?> <a href="<?php /* @escapeNotVerified */ echo $block->getFormUrl(); ?>"><?php /* @escapeNotVerified */ echo __('Modify your search.'); ?></a> </div> diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/shipping.js b/app/code/Magento/Checkout/view/frontend/web/js/view/shipping.js index 60691ab0e7b5a73bc4d3e58746be70f5ae5b070e..ddb62ceb37b37ef6bc7735c78f81367c5c6d4343 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/view/shipping.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/view/shipping.js @@ -260,11 +260,13 @@ define( this.source.trigger('shippingAddress.custom_attributes.data.validate'); } - if (this.source.get('params.invalid') || + if (emailValidationResult && + this.source.get('params.invalid') || !quote.shippingMethod().method_code || - !quote.shippingMethod().carrier_code || - !emailValidationResult + !quote.shippingMethod().carrier_code ) { + this.focusInvalid(); + return false; } diff --git a/app/code/Magento/Checkout/view/frontend/web/template/shipping.html b/app/code/Magento/Checkout/view/frontend/web/template/shipping.html index 9078731a8558f6fd2e1fde6a9201fbffc0d1fce0..5049aac2bbc9b056be66344e8b21542a2323dd68 100644 --- a/app/code/Magento/Checkout/view/frontend/web/template/shipping.html +++ b/app/code/Magento/Checkout/view/frontend/web/template/shipping.html @@ -120,7 +120,7 @@ <!-- ko if: method.error_message --> <tr class="row row-error"> <td class="col col-error" colspan="4"> - <div class="message error"> + <div role="alert" class="message error"> <div data-bind="text: method.error_message"></div> </div> <span class="no-display"> @@ -141,7 +141,7 @@ <!-- /ko --> </div> <!-- ko if: errorValidationMessage().length > 0 --> - <div class="message notice"> + <div role="alert" class="message notice"> <span><!-- ko text: errorValidationMessage()--><!-- /ko --></span> </div> <!-- /ko --> diff --git a/app/code/Magento/Cookie/view/frontend/templates/html/notices.phtml b/app/code/Magento/Cookie/view/frontend/templates/html/notices.phtml index cbf1d3ad2b231633b142127bdcf90df83d9f7bcf..f7aaf538ae96c210ae4aa7fdff0c84412207f948 100644 --- a/app/code/Magento/Cookie/view/frontend/templates/html/notices.phtml +++ b/app/code/Magento/Cookie/view/frontend/templates/html/notices.phtml @@ -9,12 +9,17 @@ /** @var \Magento\Cookie\Block\Html\Notices $block */ ?> <?php if ($this->helper(\Magento\Cookie\Helper\Cookie::class)->isUserNotAllowSaveCookie()): ?> - <div class="message global cookie" id="notice-cookie-block" style="display: none"> - <div class="content"> + <div role="alertdialog" + tabindex="-1" + class="message global cookie" + id="notice-cookie-block" + style="display: none;"> + <div role="document" class="content" tabindex="0"> <p> <strong><?php /* @escapeNotVerified */ echo __('We use cookies to make your experience better.') ?></strong> <span><?php /* @escapeNotVerified */ echo __('To comply with the new e-Privacy directive, we need to ask for your consent to set the cookies.') ?></span> - <?php /* @escapeNotVerified */ echo __('<a href="%1">Learn more</a>.', $block->getPrivacyPolicyLink()) ?></p> + <?php /* @escapeNotVerified */ echo __('<a href="%1">Learn more</a>.', $block->getPrivacyPolicyLink()) ?> + </p> <div class="actions"> <button id="btn-cookie-allow" class="action allow primary"> <span><?php /* @escapeNotVerified */ echo __('Allow Cookies');?></span> diff --git a/app/code/Magento/ImportExport/view/adminhtml/templates/busy.phtml b/app/code/Magento/ImportExport/view/adminhtml/templates/busy.phtml index cd77de6c78d861c318077a6a258f81fdada7ea63..5e6e0b8e9abf4e44aed7a92a58d603b5bde0d3f6 100644 --- a/app/code/Magento/ImportExport/view/adminhtml/templates/busy.phtml +++ b/app/code/Magento/ImportExport/view/adminhtml/templates/busy.phtml @@ -10,7 +10,7 @@ </div><br> <div class="messages"> <div class="message message-success success"> - <div><?php /* @escapeNotVerified */ echo $block->getStatusMessage(); ?>hh</div> + <div><?php /* @escapeNotVerified */ echo $block->getStatusMessage(); ?></div> </div> </div> </div> diff --git a/app/code/Magento/Swatches/view/frontend/templates/product/layered/renderer.phtml b/app/code/Magento/Swatches/view/frontend/templates/product/layered/renderer.phtml index 19b4002efb2a71289961b139cf41517c0c78e8ef..c8842b5b5be3a31f206e4cd9d98e0938a5342c2c 100644 --- a/app/code/Magento/Swatches/view/frontend/templates/product/layered/renderer.phtml +++ b/app/code/Magento/Swatches/view/frontend/templates/product/layered/renderer.phtml @@ -10,15 +10,19 @@ ?> <?php $swatchData = $block->getSwatchData(); ?> <div class="swatch-attribute swatch-layered <?php /* @escapeNotVerified */ echo $swatchData['attribute_code'] ?>" - attribute-code="<?php /* @escapeNotVerified */ echo $swatchData['attribute_code'] ?>" attribute-id="<?php /* @escapeNotVerified */ echo $swatchData['attribute_id'] ?>"> + attribute-code="<?php /* @escapeNotVerified */ echo $swatchData['attribute_code'] ?>" + attribute-id="<?php /* @escapeNotVerified */ echo $swatchData['attribute_id'] ?>"> <div class="swatch-attribute-options clearfix"> <?php foreach ($swatchData['options'] as $option => $label): ?> - <a href="<?php /* @escapeNotVerified */ echo $label['link'] ?>" class="swatch-option-link-layered"> + <a href="<?php /* @escapeNotVerified */ echo $label['link'] ?>" + aria-label="<?php /* @escapeNotVerified */ echo $label['label'] ?>" + class="swatch-option-link-layered"> <?php if (isset($swatchData['swatches'][$option]['type'])) { ?> <?php switch ($swatchData['swatches'][$option]['type']) { case '3': ?> <div class="swatch-option <?php /* @escapeNotVerified */ echo $label['custom_style'] ?>" + tabindex="-1" option-type="3" option-id="<?php /* @escapeNotVerified */ echo $option ?>" option-label="<?php /* @escapeNotVerified */ echo $label['label'] ?>" @@ -33,6 +37,7 @@ <?php $swatchImagePath = $block->getSwatchPath('swatch_image', $swatchData['swatches'][$option]['value']); ?> <div class="swatch-option image <?php /* @escapeNotVerified */ echo $label['custom_style'] ?>" + tabindex="-1" option-type="2" option-id="<?php /* @escapeNotVerified */ echo $option ?>" option-label="<?php /* @escapeNotVerified */ echo $label['label'] ?>" @@ -43,6 +48,7 @@ case '1': ?> <div class="swatch-option color <?php /* @escapeNotVerified */ echo $label['custom_style'] ?>" + tabindex="-1" option-type="1" option-id="<?php /* @escapeNotVerified */ echo $option ?>" option-label="<?php /* @escapeNotVerified */ echo $label['label'] ?>" @@ -54,6 +60,7 @@ default: ?> <div class="swatch-option text <?php /* @escapeNotVerified */ echo $label['custom_style'] ?>" + tabindex="-1" option-type="0" option-id="<?php /* @escapeNotVerified */ echo $option ?>" option-label="<?php /* @escapeNotVerified */ echo $label['label'] ?>" diff --git a/app/code/Magento/Swatches/view/frontend/web/css/swatches.css b/app/code/Magento/Swatches/view/frontend/web/css/swatches.css index 8204fc7fc8f630f64ace7caa37567550354f62cf..3899a663788b0f308ce5bf4c7b0f8da4389c4245 100644 --- a/app/code/Magento/Swatches/view/frontend/web/css/swatches.css +++ b/app/code/Magento/Swatches/view/frontend/web/css/swatches.css @@ -210,6 +210,10 @@ padding: 0 !important; } +.swatch-option-link-layered:focus > div { + box-shadow: 0 0 3px 1px #68a8e0; +} + .swatch-option-tooltip-layered { width: 140px; position: absolute; @@ -276,3 +280,9 @@ .swatch-option-loading { content: url("../images/loader-2.gif"); } + +.swatch-input { + left: -1000px; + position: absolute; + visibility: hidden; +} diff --git a/app/code/Magento/Swatches/view/frontend/web/js/swatch-renderer.js b/app/code/Magento/Swatches/view/frontend/web/js/swatch-renderer.js index dfd4b2bb2da0ffcc19286dafd125be59d980f804..4e79c42dd6adbf4eb1f079f569ac3aeb77799241 100644 --- a/app/code/Magento/Swatches/view/frontend/web/js/swatch-renderer.js +++ b/app/code/Magento/Swatches/view/frontend/web/js/swatch-renderer.js @@ -6,11 +6,57 @@ define([ 'jquery', 'underscore', + 'mage/smart-keyboard-handler', 'jquery/ui', - 'jquery/jquery.parsequery' -], function ($, _) { + 'jquery/jquery.parsequery', + 'mage/validation/validation' +], function ($, _, keyboardHandler) { 'use strict'; + /** + * Extend form validation to support swatch accessibility + */ + $.widget('mage.validation', $.mage.validation, { + /** + * Handle form with swatches validation. Focus on first invalid swatch block. + * + * @param {jQuery.Event} event + * @param {Object} validation + */ + listenFormValidateHandler: function (event, validation) { + var swatchWrapper, firstActive, swatches, swatch, successList, errorList, firstSwatch; + + this._superApply(arguments); + + swatchWrapper = '.swatch-attribute-options'; + swatches = $(event.target).find(swatchWrapper); + + if (!swatches.length) { + return; + } + + swatch = '.swatch-attribute'; + firstActive = $(validation.errorList[0].element || []); + successList = validation.successList; + errorList = validation.errorList; + firstSwatch = $(firstActive).parent(swatch).find(swatchWrapper); + + keyboardHandler.focus(swatches); + + $.each(successList, function (index, item) { + $(item).parent(swatch).find(swatchWrapper).attr('aria-invalid', false); + }); + + $.each(errorList, function (index, item) { + $(item.element).parent(swatch).find(swatchWrapper).attr('aria-invalid', true); + }); + + if (firstSwatch.length) { + $(firstSwatch).focus(); + } + } + }); + /** * Render tooltips by attributes (only to up). * Required element attributes: @@ -117,6 +163,7 @@ define([ $element.hide(); clearTimeout(timer); }); + $(document).on('tap', function () { $element.hide(); clearTimeout(timer); @@ -169,6 +216,9 @@ define([ //selector of product images gallery wrapper mediaGallerySelector: '[data-gallery-role=gallery-placeholder]', + // selector of category product tile wrapper + selectorProductTile: '.product-item', + // number of controls to show (false or zero = show all) numberToShow: false, @@ -178,6 +228,9 @@ define([ // enable label for control enableControlLabel: true, + // control label id + controlLabelId: '', + // text for more button moreButtonText: 'More', @@ -191,7 +244,10 @@ define([ mediaGalleryInitial: [{}], // - onlyMainImg: false + onlyMainImg: false, + + // whether swatches are rendered in product list or on product page + inProductList: false }, /** @@ -246,7 +302,9 @@ define([ 'img': $main.find('.product-image-photo').attr('src') }]; } - this.productForm = this.element.parents(this.options.selectorProduct).find('form:first'); + + this.productForm = this.element.parents(this.options.selectorProductTile).find('form:first'); + this.inProductList = this.productForm.length > 0; }, /** @@ -264,9 +322,11 @@ define([ $.each(this.options.jsonConfig.attributes, function () { var item = this, - options = $widget._RenderSwatchOptions(item), + controlLabelId = 'option-label-' + item.code + '-' + item.id, + options = $widget._RenderSwatchOptions(item, controlLabelId), select = $widget._RenderSwatchSelect(item, chooseText), input = $widget._RenderFormInput(item), + listLabel = '', label = ''; // Show only swatch controls @@ -276,22 +336,32 @@ define([ if ($widget.options.enableControlLabel) { label += - '<span class="' + classes.attributeLabelClass + '">' + item.label + '</span>' + + '<span id="' + controlLabelId + '" class="' + classes.attributeLabelClass + '">' + + item.label + + '</span>' + '<span class="' + classes.attributeSelectedOptionLabelClass + '"></span>'; } - if ($widget.productForm) { + if ($widget.inProductList) { $widget.productForm.append(input); input = ''; + listLabel = 'aria-label="' + item.label + '"'; + } else { + listLabel = 'aria-labelledby="' + controlLabelId + '"'; } // Create new control container.append( - '<div class="' + classes.attributeClass + ' ' + item.code + - '" attribute-code="' + item.code + - '" attribute-id="' + item.id + '">' + - label + - '<div class="' + classes.attributeOptionsWrapper + ' clearfix">' + + '<div class="' + classes.attributeClass + ' ' + item.code + '" ' + + 'attribute-code="' + item.code + '" ' + + 'attribute-id="' + item.id + '">' + + label + + '<div aria-activedescendant="" ' + + 'tabindex="0" ' + + 'aria-invalid="false" ' + + 'aria-required="true" ' + + 'role="listbox" ' + listLabel + + 'class="' + classes.attributeOptionsWrapper + ' clearfix">' + options + select + '</div>' + input + '</div>' @@ -336,10 +406,11 @@ define([ * Render swatch options by part of config * * @param {Object} config + * @param {String} controlId * @returns {String} * @private */ - _RenderSwatchOptions: function (config) { + _RenderSwatchOptions: function (config, controlId) { var optionConfig = this.options.jsonSwatchConfig[config.id], optionClass = this.options.classes.optionClass, moreLimit = parseInt(this.options.numberToShow, 10), @@ -375,11 +446,17 @@ define([ thumb = optionConfig[id].hasOwnProperty('thumb') ? optionConfig[id].thumb : ''; label = this.label ? this.label : ''; attr = + ' id="' + controlId + '-item-' + id + '"' + + ' aria-checked="false"' + + ' aria-describedby="' + controlId + '"' + + ' tabindex="0"' + ' option-type="' + type + '"' + ' option-id="' + id + '"' + ' option-label="' + label + '"' + + ' aria-label="' + label + '"' + ' option-tooltip-thumb="' + thumb + '"' + - ' option-tooltip-value="' + value + '"'; + ' option-tooltip-value="' + value + '"' + + ' role="option"'; if (!this.hasOwnProperty('products') || this.products.length <= 0) { attr += ' option-empty="true"'; @@ -392,19 +469,19 @@ define([ } else if (type === 1) { // Color html += '<div class="' + optionClass + ' color" ' + attr + - '" style="background: ' + value + + ' style="background: ' + value + ' no-repeat center; background-size: initial;">' + '' + '</div>'; } else if (type === 2) { // Image html += '<div class="' + optionClass + ' image" ' + attr + - '" style="background: url(' + value + ') no-repeat center; background-size: initial;">' + '' + + ' style="background: url(' + value + ') no-repeat center; background-size: initial;">' + '' + '</div>'; } else if (type === 3) { // Clear html += '<div class="' + optionClass + '" ' + attr + '></div>'; } else { - // Defaualt + // Default html += '<div class="' + optionClass + '" ' + attr + '>' + label + '</div>'; } }); @@ -460,10 +537,9 @@ define([ 'type="text" ' + 'value="" ' + 'data-selector="super_attribute[' + config.id + ']" ' + - 'data-validate="{required:true}" ' + + 'data-validate="{required: true}" ' + 'aria-required="true" ' + - 'aria-invalid="true" ' + - 'style="visibility: hidden; position:absolute; left:-1000px">'; + 'aria-invalid="false">'; }, /** @@ -472,22 +548,39 @@ define([ * @private */ _EventListener: function () { + var $widget = this, + options = this.options.classes, + target; - var $widget = this; - - $widget.element.on('click', '.' + this.options.classes.optionClass, function () { + $widget.element.on('click', '.' + options.optionClass, function () { return $widget._OnClick($(this), $widget); }); - $widget.element.on('change', '.' + this.options.classes.selectClass, function () { + $widget.element.on('change', '.' + options.selectClass, function () { return $widget._OnChange($(this), $widget); }); - $widget.element.on('click', '.' + this.options.classes.moreButton, function (e) { + $widget.element.on('click', '.' + options.moreButton, function (e) { e.preventDefault(); return $widget._OnMoreClick($(this)); }); + + $widget.element.on('keydown', function (e) { + if (e.which === 13) { + target = $(e.target); + + if (target.is('.' + options.optionClass)) { + return $widget._OnClick(target, $widget); + } else if (target.is('.' + options.selectClass)) { + return $widget._OnChange(target, $widget); + } else if (target.is('.' + options.moreButton)) { + e.preventDefault(); + + return $widget._OnMoreClick(target); + } + } + }); }, /** @@ -498,13 +591,17 @@ define([ * @private */ _OnClick: function ($this, $widget) { - var $parent = $this.parents('.' + $widget.options.classes.attributeClass), + $wrapper = $this.parents('.' + $widget.options.classes.attributeOptionsWrapper), $label = $parent.find('.' + $widget.options.classes.attributeSelectedOptionLabelClass), attributeId = $parent.attr('attribute-id'), + $input = $parent.find('.' + $widget.options.classes.attributeInput); + + if ($widget.inProductList) { $input = $widget.productForm.find( '.' + $widget.options.classes.attributeInput + '[name="super_attribute[' + attributeId + ']"]' ); + } if ($this.hasClass('disabled')) { return; @@ -514,11 +611,13 @@ define([ $parent.removeAttr('option-selected').find('.selected').removeClass('selected'); $input.val(''); $label.text(''); + $this.attr('aria-checked', false); } else { $parent.attr('option-selected', $this.attr('option-id')).find('.selected').removeClass('selected'); $label.text($this.attr('option-label')); $input.val($this.attr('option-id')); $this.addClass('selected'); + $widget._toggleCheckedAttributes($this, $wrapper); } $widget._Rebuild(); @@ -533,6 +632,19 @@ define([ $input.trigger('change'); }, + /** + * Toggle accessibility attributes + * + * @param {Object} $this + * @param {Object} $wrapper + * @private + */ + _toggleCheckedAttributes: function ($this, $wrapper) { + $wrapper.attr('aria-activedescendant', $this.attr('id')) + .find('.' + this.options.classes.optionClass).attr('aria-checked', false); + $this.attr('aria-checked', true); + }, + /** * Event for select * @@ -543,9 +655,13 @@ define([ _OnChange: function ($this, $widget) { var $parent = $this.parents('.' + $widget.options.classes.attributeClass), attributeId = $parent.attr('attribute-id'), + $input = $parent.find('.' + $widget.options.classes.attributeInput); + + if ($widget.productForm.length > 0) { $input = $widget.productForm.find( '.' + $widget.options.classes.attributeInput + '[name="super_attribute[' + attributeId + ']"]' ); + } if ($this.val() > 0) { $parent.attr('option-selected', $this.val()); @@ -687,7 +803,6 @@ define([ 'prices': $widget._getPrices(result, $productPrice.priceBox('option').prices) } ); - }, /** diff --git a/app/code/Magento/Theme/view/frontend/templates/messages.phtml b/app/code/Magento/Theme/view/frontend/templates/messages.phtml index 2bd2357a27e1adbf79a9dbf7c34cc9c6d2d0da29..7ef534ab5d65c5b1df7014509e4ab7b3b41c3884 100644 --- a/app/code/Magento/Theme/view/frontend/templates/messages.phtml +++ b/app/code/Magento/Theme/view/frontend/templates/messages.phtml @@ -5,7 +5,8 @@ */ ?> <div data-bind="scope: 'messages'"> - <div data-bind="foreach: { data: cookieMessages, as: 'message' }" class="messages"> + <!-- ko if: cookieMessages && cookieMessages.length > 0 --> + <div role="alert" data-bind="foreach: { data: cookieMessages, as: 'message' }" class="messages"> <div data-bind="attr: { class: 'message-' + message.type + ' ' + message.type + ' message', 'data-ui-id': 'message-' + message.type @@ -13,7 +14,9 @@ <div data-bind="html: message.text"></div> </div> </div> - <div data-bind="foreach: { data: messages().messages, as: 'message' }" class="messages"> + <!-- /ko --> + <!-- ko if: messages().messages && messages().messages.length > 0 --> + <div role="alert" data-bind="foreach: { data: messages().messages, as: 'message' }" class="messages"> <div data-bind="attr: { class: 'message-' + message.type + ' ' + message.type + ' message', 'data-ui-id': 'message-' + message.type @@ -21,6 +24,7 @@ <div data-bind="html: message.text"></div> </div> </div> + <!-- /ko --> </div> <script type="text/x-magento-init"> { diff --git a/app/code/Magento/Ui/view/base/web/js/form/element/abstract.js b/app/code/Magento/Ui/view/base/web/js/form/element/abstract.js index 4596569a7b6ac4a98bd727e8a7c1ad273a8889cc..151219b87abc5d540a35cc4e0198688bb6f6aca8 100755 --- a/app/code/Magento/Ui/view/base/web/js/form/element/abstract.js +++ b/app/code/Magento/Ui/view/base/web/js/form/element/abstract.js @@ -24,7 +24,7 @@ define([ tooltipTpl: 'ui/form/element/helper/tooltip', fallbackResetTpl: 'ui/form/element/helper/fallback-reset', 'input_type': 'input', - placeholder: '', + placeholder: false, description: '', labelVisible: true, label: '', @@ -74,6 +74,15 @@ define([ return this; }, + /** + * Checks if component has error. + * + * @returns {Object} + */ + checkInvalid: function () { + return this.error() && this.error().length ? this : null; + }, + /** * Initializes observable properties of instance * @@ -84,7 +93,7 @@ define([ this._super(); - this.observe('error disabled focused preview visible value warn isDifferedFromDefault') + this.observe('error disabled focused preview visible value warn notice isDifferedFromDefault') .observe('isUseDefault') .observe({ 'required': !!rules['required-entry'] @@ -114,6 +123,7 @@ define([ _.extend(this, { uid: uid, noticeId: 'notice-' + uid, + errorId: 'error-' + uid, inputName: utils.serializeName(name.join('.')), valueUpdate: valueUpdate }); @@ -440,6 +450,23 @@ define([ */ userChanges: function () { this.valueChangedByUser = true; + }, + + /** + * Returns correct id for 'aria-describedby' accessibility attribute + * + * @returns {Boolean|String} + */ + getDescriptionId: function () { + var id = false; + + if (this.error()) { + id = this.errorId; + } else if (this.notice()) { + id = this.noticeId; + } + + return id; } }); }); diff --git a/app/code/Magento/Ui/view/base/web/js/form/form.js b/app/code/Magento/Ui/view/base/web/js/form/form.js index 672af8a7ed845a12efe19f0801325e23c06932a6..0a3d6ee5850c425b861cb084761cde1cf49df2fb 100644 --- a/app/code/Magento/Ui/view/base/web/js/form/form.js +++ b/app/code/Magento/Ui/view/base/web/js/form/form.js @@ -248,19 +248,31 @@ define([ * @param {Object} data */ save: function (redirect, data) { - var scrollTop; - this.validate(); if (!this.additionalInvalid && !this.source.get('params.invalid')) { this.setAdditionalData(data) .submit(redirect); } else { - scrollTop = $(this.errorClass).offset().top - window.innerHeight / 2; - window.scrollTo(0, scrollTop); + this.focusInvalid(); } }, + /** + * Tries to set focus on first invalid form field. + * + * @returns {Object} + */ + focusInvalid: function () { + var invalidField = _.find(this.delegate('checkInvalid')); + + if (!_.isUndefined(invalidField) && _.isFunction(invalidField.focused)) { + invalidField.focused(true); + } + + return this; + }, + /** * Set additional data to source before form submit and after validation. * diff --git a/app/code/Magento/Ui/view/base/web/js/grid/editing/editor.js b/app/code/Magento/Ui/view/base/web/js/grid/editing/editor.js index b39fb1a371801f7f4a06c786341943ee0d245c41..0fc16010af36321c382d906a3312207d53d5e135 100644 --- a/app/code/Magento/Ui/view/base/web/js/grid/editing/editor.js +++ b/app/code/Magento/Ui/view/base/web/js/grid/editing/editor.js @@ -486,7 +486,7 @@ define([ }, /** - * Counts number of invalid fields accros all active records. + * Counts number of invalid fields across all active records. * * @returns {Number} */ @@ -502,6 +502,16 @@ define([ return errorsCount; }, + /** + * Translatable error message text. + * + * @returns {String} + */ + countErrorsMessage: function () { + return $t('There are {placeholder} messages requires your attention.') + .replace('{placeholder}', this.countErrors()); + }, + /** * Checks if editor has any errors. * diff --git a/app/code/Magento/Ui/view/base/web/templates/form/field.html b/app/code/Magento/Ui/view/base/web/templates/form/field.html index adc9814a91d0ee3cbe5a1c955b532d385536fd8f..4a9d8f8c75a77d266297dfec620d548a7a12ee76 100644 --- a/app/code/Magento/Ui/view/base/web/templates/form/field.html +++ b/app/code/Magento/Ui/view/base/web/templates/form/field.html @@ -40,4 +40,4 @@ <render args="$data.service.template" if="$data.hasService()"/> </div> -</div> \ No newline at end of file +</div> diff --git a/app/code/Magento/Ui/view/base/web/templates/grid/editing/header-buttons.html b/app/code/Magento/Ui/view/base/web/templates/grid/editing/header-buttons.html index ab0131ed667cfefb4074e47c5aa69727e039d16f..fd4e61108aae0ef8e34e6a713207cb94a4560049 100644 --- a/app/code/Magento/Ui/view/base/web/templates/grid/editing/header-buttons.html +++ b/app/code/Magento/Ui/view/base/web/templates/grid/editing/header-buttons.html @@ -4,19 +4,21 @@ * See COPYING.txt for license details. */ --> -<div class="data-grid-info-panel" visible="isMultiEditing || (hasActive() && (hasMessages() || hasErrors() ))"> - <div class="messages" visible="hasMessages() || hasErrors()"> - <div class="message message-warning" visible="hasErrors()"> - <strong>There are <text args="countErrors()"/> messages requires your attention.</strong> - Please make corrections to the errors in the table below and re-submit. +<div if="isMultiEditing || (hasActive() && (hasMessages() || hasErrors() ))" + attr="role: (isMultiEditing && multiEditingButtons) ? 'alertdialog' : 'alert'" + class="data-grid-info-panel"> + <div if="hasMessages() || hasErrors()" class="messages"> + <div if="hasErrors()" class="message message-warning"> + <strong><text args="countErrorsMessage()"/></strong> + <span translate="'Please make corrections to the errors in the table below and re-submit.'"/> </div> <div class="message" outereach="messages" text="message" - css=" - 'message-warning': type === 'warning', - 'message-error': type === 'error', - 'message-success': type === 'success'"/> + css=" + 'message-warning': type === 'warning', + 'message-error': type === 'error', + 'message-success': type === 'success'"/> </div> - <div class="data-grid-info-panel-actions" visible="isMultiEditing && multiEditingButtons"> + <div if="isMultiEditing && multiEditingButtons" class="data-grid-info-panel-actions"> <button class="action-tertiary" type="button" click="cancel"> <span translate="'Cancel'"/> </button> diff --git a/app/code/Magento/Ui/view/frontend/web/template/messages.html b/app/code/Magento/Ui/view/frontend/web/template/messages.html index 5966523ec89206d829f2da18784ea756d83e9492..f0f03ec2e53257d2965a3acbed96d95faa1ba860 100644 --- a/app/code/Magento/Ui/view/frontend/web/template/messages.html +++ b/app/code/Magento/Ui/view/frontend/web/template/messages.html @@ -6,13 +6,13 @@ --> <div data-role="checkout-messages" class="messages" data-bind="visible: isVisible(), click: removeAll"> <!-- ko foreach: messageContainer.getErrorMessages() --> - <div class="message message-error error"> + <div role="alert" class="message message-error error"> <div data-ui-id="checkout-cart-validationmessages-message-error" data-bind="text: $data"></div> </div> <!--/ko--> <!-- ko foreach: messageContainer.getSuccessMessages() --> - <div class="message message-success success"> + <div role="alert" class="message message-success success"> <div data-ui-id="checkout-cart-validationmessages-message-success" data-bind="text: $data"></div> </div> <!--/ko--> -</div> \ No newline at end of file +</div> diff --git a/app/code/Magento/Ui/view/frontend/web/templates/form/element/checkbox.html b/app/code/Magento/Ui/view/frontend/web/templates/form/element/checkbox.html index 50a27e6902528938e66ab2e632aa98ef23993701..c22df869a0013a9dd8960cada1ab7fb1b2601587 100644 --- a/app/code/Magento/Ui/view/frontend/web/templates/form/element/checkbox.html +++ b/app/code/Magento/Ui/view/frontend/web/templates/form/element/checkbox.html @@ -5,7 +5,19 @@ */ --> <div class="choice field"> - <input type="checkbox" class="checkbox" data-bind="checked: value, attr: { id: uid, disabled: disabled, name: inputName }, hasFocus: focused"> + <input type="checkbox" + class="checkbox" + data-bind=" + checked: value, + attr: { + id: uid, + disabled: disabled, + name: inputName, + 'aria-describedby': getDescriptionId(), + 'aria-required': required, + 'aria-invalid': error() ? true : 'false' + }, + hasFocus: focused"> <label class="label" data-bind="checked: value, attr: { for: uid }"> <span data-bind="text: description || label"></span> diff --git a/app/code/Magento/Ui/view/frontend/web/templates/form/element/date.html b/app/code/Magento/Ui/view/frontend/web/templates/form/element/date.html index be2034a6a0b9710b0bc044a2334bf0744c5c1781..48faf4b73a5f5e4a4987bbc9c4b33595e74bed3c 100644 --- a/app/code/Magento/Ui/view/frontend/web/templates/form/element/date.html +++ b/app/code/Magento/Ui/view/frontend/web/templates/form/element/date.html @@ -11,6 +11,8 @@ value: value, name: inputName, placeholder: placeholder, - 'aria-describedby': noticeId, + 'aria-describedby': getDescriptionId(), + 'aria-required': required, + 'aria-invalid': error() ? true : 'false', disabled: disabled }" /> diff --git a/app/code/Magento/Ui/view/frontend/web/templates/form/element/email.html b/app/code/Magento/Ui/view/frontend/web/templates/form/element/email.html index dfd75630e08bc086d7cf87dedf55000cb9ffc10c..57f94670323ad25c2cd5b8038eee5a7d4f984c3f 100644 --- a/app/code/Magento/Ui/view/frontend/web/templates/form/element/email.html +++ b/app/code/Magento/Ui/view/frontend/web/templates/form/element/email.html @@ -10,7 +10,9 @@ attr: { name: inputName, placeholder: placeholder, - 'aria-describedby': noticeId, + 'aria-describedby': getDescriptionId(), + 'aria-required': required, + 'aria-invalid': error() ? true : 'false', id: uid, disabled: disabled }"/> diff --git a/app/code/Magento/Ui/view/frontend/web/templates/form/element/input.html b/app/code/Magento/Ui/view/frontend/web/templates/form/element/input.html index 01234333d5816573d1c2ae291446229e8a317e9b..a09e0383ef81d65103bbc3c53182c6740b7d3587 100644 --- a/app/code/Magento/Ui/view/frontend/web/templates/form/element/input.html +++ b/app/code/Magento/Ui/view/frontend/web/templates/form/element/input.html @@ -11,7 +11,9 @@ attr: { name: inputName, placeholder: placeholder, - 'aria-describedby': noticeId, + 'aria-describedby': getDescriptionId(), + 'aria-required': required, + 'aria-invalid': error() ? true : 'false', id: uid, disabled: disabled }" /> diff --git a/app/code/Magento/Ui/view/frontend/web/templates/form/element/password.html b/app/code/Magento/Ui/view/frontend/web/templates/form/element/password.html index 825a9869ec2efbf9be3ad4492b972cb30946ec9f..0d93d48aa96f6d7d10a7b02160aded2a489cf442 100644 --- a/app/code/Magento/Ui/view/frontend/web/templates/form/element/password.html +++ b/app/code/Magento/Ui/view/frontend/web/templates/form/element/password.html @@ -10,7 +10,9 @@ attr: { name: inputName, placeholder: placeholder, - 'aria-describedby': noticeId, + 'aria-describedby': getDescriptionId(), + 'aria-required': required, + 'aria-invalid': error() ? true : 'false', id: uid, disabled: disabled }"/> diff --git a/app/code/Magento/Ui/view/frontend/web/templates/form/element/select.html b/app/code/Magento/Ui/view/frontend/web/templates/form/element/select.html index edd1395c5a719a0050b93afdcc16628b3df40f97..37d87d183e023574f4a1485649fd5046aa43f8fd 100644 --- a/app/code/Magento/Ui/view/frontend/web/templates/form/element/select.html +++ b/app/code/Magento/Ui/view/frontend/web/templates/form/element/select.html @@ -9,7 +9,9 @@ name: inputName, id: uid, disabled: disabled, - 'aria-describedby': noticeId, + 'aria-describedby': getDescriptionId(), + 'aria-required': required, + 'aria-invalid': error() ? true : 'false', placeholder: placeholder }, hasFocus: focused, diff --git a/app/code/Magento/Ui/view/frontend/web/templates/form/field.html b/app/code/Magento/Ui/view/frontend/web/templates/form/field.html index f3be62f116b8358e9424934951dc9c40a4a2d22a..d0bc45c74dbb41173454e0fff3f315b06841505c 100644 --- a/app/code/Magento/Ui/view/frontend/web/templates/form/field.html +++ b/app/code/Magento/Ui/view/frontend/web/templates/form/field.html @@ -36,16 +36,22 @@ <!-- /ko --> <!-- ko if: element.notice --> - <div class="field-note" data-bind="attr: { id: element.noticeId }"><span data-bind="text: element.notice"></span></div> + <div class="field-note" data-bind="attr: { id: element.noticeId }"> + <span data-bind="text: element.notice"/> + </div> <!-- /ko --> <!-- ko if: element.error() --> - <div class="mage-error" data-bind="attr: { for: element.uid }, text: element.error" generated="true"></div> + <div class="field-error" data-bind="attr: { id: element.errorId }" generated="true"> + <span data-bind="text: element.error"/> + </div> <!-- /ko --> <!-- ko if: element.warn() --> - <div class="message warning" generated="true"><span data-bind="text: element.warn"></span></div> + <div role="alert" class="message warning" data-bind="attr: { id: element.warningId }" generated="true"> + <span data-bind="text: element.warn"/> + </div> <!-- /ko --> </div> </div> -<!-- /ko --> \ No newline at end of file +<!-- /ko --> diff --git a/app/design/frontend/Magento/blank/Magento_Theme/web/css/source/_module.less b/app/design/frontend/Magento/blank/Magento_Theme/web/css/source/_module.less index 3b8e84da841069ed41c651329f48ef91a0e10133..7fb7985ada12a8f932134c8afeee765a260f6083 100644 --- a/app/design/frontend/Magento/blank/Magento_Theme/web/css/source/_module.less +++ b/app/design/frontend/Magento/blank/Magento_Theme/web/css/source/_module.less @@ -112,6 +112,11 @@ } } + .action-skip-wrapper { + height: 0; + position: relative; + } + // // Global notice // --------------------------------------------- diff --git a/app/design/frontend/Magento/blank/web/css/source/_forms.less b/app/design/frontend/Magento/blank/web/css/source/_forms.less index 72e014b8305aaf86e0405df59654c8faf1297834..8c7258c390ab723e4f55492f42cffccfc8f60a09 100644 --- a/app/design/frontend/Magento/blank/web/css/source/_forms.less +++ b/app/design/frontend/Magento/blank/web/css/source/_forms.less @@ -92,10 +92,15 @@ } } + .field-error, div.mage-error[generated] { margin-top: 7px; } + .field-error { + .lib-form-validation-note(); + } + .field .tooltip { .lib-tooltip(right); .tooltip-content { diff --git a/app/design/frontend/Magento/luma/Magento_Theme/web/css/source/_module.less b/app/design/frontend/Magento/luma/Magento_Theme/web/css/source/_module.less index 5581eb7fb05212c4c24967baf6aab30398bd8072..12ef890f4305539539582af433c1b7961a15043a 100644 --- a/app/design/frontend/Magento/luma/Magento_Theme/web/css/source/_module.less +++ b/app/design/frontend/Magento/luma/Magento_Theme/web/css/source/_module.less @@ -168,6 +168,11 @@ } } + .action-skip-wrapper { + height: 0; + position: relative; + } + // // Global notice // --------------------------------------------- diff --git a/app/design/frontend/Magento/luma/web/css/source/_forms.less b/app/design/frontend/Magento/luma/web/css/source/_forms.less index 8d22eba9ca51f892425a60b73a43cedc6a149fa1..d29e53d7fefe68622af55b4004c3742df2be93d5 100644 --- a/app/design/frontend/Magento/luma/web/css/source/_forms.less +++ b/app/design/frontend/Magento/luma/web/css/source/_forms.less @@ -113,10 +113,15 @@ .select-styling(); } + .field-error, div.mage-error[generated] { margin-top: 7px; } + .field-error { + .lib-form-validation-note(); + } + // TEMP .field .tooltip { diff --git a/composer.json b/composer.json index 8053b76a01a2283f5f1ad39ec8028123bca9bb14..a0813c8462066d0eabcacb4a21cdb7b5a09dbd13 100644 --- a/composer.json +++ b/composer.json @@ -64,6 +64,7 @@ "ext-mbstring": "*", "ext-openssl": "*", "ext-zip": "*", + "ext-pdo_mysql": "*", "sjparkinson/static-review": "~4.1", "ramsey/uuid": "3.4" }, diff --git a/composer.lock b/composer.lock index 6fd73ef405c822bcb049709ec1150c6e51254e35..4d1f23dad9b512b00d28341d28d1db5ac05899bb 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,8 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "ad3de2234f78fd4b353ae6a1b22401fc", - "content-hash": "25dcf96ed1d8b12a25111e8b3af61317", + "hash": "c8f9e4332a46ace884514d9843021898", + "content-hash": "fa9bd2b88f3c2e1bf562c439cecca917", "packages": [ { "name": "braintree/braintree_php", @@ -4597,7 +4597,8 @@ "ext-xsl": "*", "ext-mbstring": "*", "ext-openssl": "*", - "ext-zip": "*" + "ext-zip": "*", + "ext-pdo_mysql": "*" }, "platform-dev": [] } diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Product/View.php b/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Product/View.php index 9b57ce34f4eea73928920c87dfdd1ebab23c81d1..3134fda0d2b2b3f98b3e9d0ef5c835e8cb1aad53 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Product/View.php +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Product/View.php @@ -567,12 +567,19 @@ class View extends AbstractConfigureBlock */ public function closeFullImage() { - $element = $this->browser->find($this->fullImageClose, Locator::SELECTOR_CSS); - if (!$element->isVisible()) { - $element->hover(); - $this->waitForElementVisible($this->fullImageClose); - } - $element->click(); + $this->_rootElement->waitUntil( + function () { + $this->browser->find($this->fullImage)->hover(); + + if ($this->browser->find($this->fullImageClose)->isVisible()) { + $this->browser->find($this->fullImageClose)->click(); + + return true; + } + + return null; + } + ); } /** diff --git a/lib/web/mage/smart-keyboard-handler.js b/lib/web/mage/smart-keyboard-handler.js index 859ab69372e09515f967a10a42c421ea7b750341..b6a65f2ee5d43b43ae91722b43507c39febd1941 100644 --- a/lib/web/mage/smart-keyboard-handler.js +++ b/lib/web/mage/smart-keyboard-handler.js @@ -16,7 +16,8 @@ define([ CODE_TAB = 9; return { - apply: smartKeyboardFocus + apply: smartKeyboardFocus, + focus: handleFocus }; /** @@ -49,22 +50,39 @@ define([ * Handle logic, when onTabKeyPress fired at first. * Then it changes state. */ - function onFocusInHandler () { + function onFocusInHandler() { focusState = true; - $('body').addClass(tabFocusClass) + body.addClass(tabFocusClass) .off('focusin.keyboardHandler', onFocusInHandler); } /** * Handle logic to remove state after onTabKeyPress to normal. - * @param {Event} event */ - function onClickHandler(event) { - focusState = false; - $('body').removeClass(tabFocusClass) + function onClickHandler() { + focusState = false; + body.removeClass(tabFocusClass) .off('click', onClickHandler); } + + /** + * Attach smart focus on specific element. + * @param {jQuery} element + */ + function handleFocus(element) { + element.on('focusin.emulateTabFocus', function () { + focusState = true; + body.addClass(tabFocusClass); + element.off(); + }); + + element.on('focusout.emulateTabFocus', function () { + focusState = false; + body.removeClass(tabFocusClass); + element.off(); + }); + } } return new KeyboardHandler; -}); \ No newline at end of file +}); diff --git a/lib/web/mage/validation.js b/lib/web/mage/validation.js index 6c8deba8164112a31939b11d1ed79b5c23dd7bc0..df2cd0288648e4a33b6e724c4141baf31d3a4d17 100644 --- a/lib/web/mage/validation.js +++ b/lib/web/mage/validation.js @@ -1543,6 +1543,7 @@ this.validate.resetForm(); } }, + /** * Validation creation * @protected @@ -1559,36 +1560,46 @@ this._listenFormValidate(); }, + /** * Validation listening * @protected */ _listenFormValidate: function () { - $('form').on('invalid-form.validate', function (event, validation) { - var firstActive = $(validation.errorList[0].element || []), - lastActive = $(validation.findLastActive() || validation.errorList.length && validation.errorList[0].element || []); - - if (lastActive.is(':hidden')) { - var parent = lastActive.parent(); - var windowHeight = $(window).height(); - $('html, body').animate({ - scrollTop: parent.offset().top - windowHeight / 2 - }); - } - - // ARIA (removing aria attributes if success) - var successList = validation.successList; - if (successList.length) { - $.each(successList, function () { - $(this) - .removeAttr('aria-describedby') - .removeAttr('aria-invalid'); - }) - } - if (firstActive.length) { - firstActive.focus(); - } - }); + $('form').on('invalid-form.validate', this.listenFormValidateHandler); + }, + + /** + * Handle form validation. Focus on first invalid form field. + * + * @param {jQuery.Event} event + * @param {Object} validation + */ + listenFormValidateHandler: function (event, validation) { + var firstActive = $(validation.errorList[0].element || []), + lastActive = $(validation.findLastActive() || validation.errorList.length && validation.errorList[0].element || []); + + if (lastActive.is(':hidden')) { + var parent = lastActive.parent(); + var windowHeight = $(window).height(); + $('html, body').animate({ + scrollTop: parent.offset().top - windowHeight / 2 + }); + } + + // ARIA (removing aria attributes if success) + var successList = validation.successList; + if (successList.length) { + $.each(successList, function () { + $(this) + .removeAttr('aria-describedby') + .removeAttr('aria-invalid'); + }); + } + + if (firstActive.length) { + firstActive.focus(); + } } }); diff --git a/setup/src/Magento/Setup/Model/PhpReadinessCheck.php b/setup/src/Magento/Setup/Model/PhpReadinessCheck.php index 06e8b34471dada4264aef7be5dcba41febc14dd8..74b2afd1419c440977f2b10418964286eb738b39 100644 --- a/setup/src/Magento/Setup/Model/PhpReadinessCheck.php +++ b/setup/src/Magento/Setup/Model/PhpReadinessCheck.php @@ -102,7 +102,8 @@ class PhpReadinessCheck $settings = array_merge( $this->checkXDebugNestedLevel(), - $this->checkPopulateRawPostSetting() + $this->checkPopulateRawPostSetting(), + $this->checkFunctionsExistence() ); foreach ($settings as $setting) { @@ -316,6 +317,33 @@ class PhpReadinessCheck return $data; } + /** + * Check whether all special functions exists + * + * @return array + */ + private function checkFunctionsExistence() + { + $data = []; + $requiredFunctions = [ + [ + 'name' => 'imagecreatefromjpeg', + 'message' => 'You must have installed GD library with --with-jpeg-dir=DIR option.', + 'helpUrl' => 'http://php.net/manual/en/image.installation.php', + ], + ]; + + foreach ($requiredFunctions as $function) { + $data['missed_function_' . $function['name']] = [ + 'message' => $function['message'], + 'helpUrl' => $function['helpUrl'], + 'error' => !function_exists($function['name']), + ]; + } + + return $data; + } + /** * Normalize PHP Version * diff --git a/setup/src/Magento/Setup/Test/Unit/Model/PhpReadinessCheckTest.php b/setup/src/Magento/Setup/Test/Unit/Model/PhpReadinessCheckTest.php index c24e1792331ef09c20ac5549fc9d7edb84c8a3c1..a77bd46c79f25495570070968ae62ebb0762ef34 100644 --- a/setup/src/Magento/Setup/Test/Unit/Model/PhpReadinessCheckTest.php +++ b/setup/src/Magento/Setup/Test/Unit/Model/PhpReadinessCheckTest.php @@ -222,7 +222,12 @@ class PhpReadinessCheckTest extends \PHPUnit_Framework_TestCase 'message' => $xdebugMessage, 'error' => false, ], - ] + 'missed_function_imagecreatefromjpeg' => [ + 'message' => 'You must have installed GD library with --with-jpeg-dir=DIR option.', + 'helpUrl' => 'http://php.net/manual/en/image.installation.php', + 'error' => false, + ], + ], ]; if (!$this->isPhp7OrHhvm()) { $this->setUpNoPrettyVersionParser(); @@ -261,8 +266,13 @@ class PhpReadinessCheckTest extends \PHPUnit_Framework_TestCase 'xdebug_max_nesting_level' => [ 'message' => $xdebugMessage, 'error' => true, - ] - ] + ], + 'missed_function_imagecreatefromjpeg' => [ + 'message' => 'You must have installed GD library with --with-jpeg-dir=DIR option.', + 'helpUrl' => 'http://php.net/manual/en/image.installation.php', + 'error' => false, + ], + ], ]; if (!$this->isPhp7OrHhvm()) { $this->setUpNoPrettyVersionParser(); @@ -301,6 +311,13 @@ class PhpReadinessCheckTest extends \PHPUnit_Framework_TestCase ] ]; } + + $expected['data']['missed_function_imagecreatefromjpeg'] = [ + 'message' => 'You must have installed GD library with --with-jpeg-dir=DIR option.', + 'helpUrl' => 'http://php.net/manual/en/image.installation.php', + 'error' => false, + ]; + $this->assertEquals($expected, $this->phpReadinessCheck->checkPhpSettings()); }