From f1665ea9285af73c4cbac7f00ef3d913baaabce9 Mon Sep 17 00:00:00 2001 From: bojanz Date: Thu, 14 Sep 2017 22:13:22 +0200 Subject: [PATCH] Issue #2902408 by chrisrockwell, bojanz: Applying a coupon saves the other checkout panes --- .../CouponRedemptionPaneTest.php | 59 ++++++++++++++++++- src/Element/CommerceElementTrait.php | 43 ++++++++++++-- 2 files changed, 96 insertions(+), 6 deletions(-) diff --git a/modules/promotion/tests/src/FunctionalJavascript/CouponRedemptionPaneTest.php b/modules/promotion/tests/src/FunctionalJavascript/CouponRedemptionPaneTest.php index 83cfd3a6d8..3faa3bad07 100644 --- a/modules/promotion/tests/src/FunctionalJavascript/CouponRedemptionPaneTest.php +++ b/modules/promotion/tests/src/FunctionalJavascript/CouponRedemptionPaneTest.php @@ -102,7 +102,19 @@ protected function setUp() { $this->promotion->save(); /** @var \Drupal\commerce_payment\Entity\PaymentGateway $gateway */ - $gateway = PaymentGateway::create([ + $offsite_gateway = PaymentGateway::create([ + 'id' => 'offsite', + 'label' => 'Off-site', + 'plugin' => 'example_offsite_redirect', + 'configuration' => [ + 'redirect_method' => 'post', + 'payment_method_types' => ['credit_card'], + ], + ]); + $offsite_gateway->save(); + + /** @var \Drupal\commerce_payment\Entity\PaymentGateway $gateway */ + $onsite_gateway = PaymentGateway::create([ 'id' => 'onsite', 'label' => 'On-site', 'plugin' => 'example_onsite', @@ -111,7 +123,7 @@ protected function setUp() { 'payment_method_types' => ['credit_card'], ], ]); - $gateway->save(); + $onsite_gateway->save(); $profile = $this->createEntity('profile', [ 'type' => 'customer', @@ -267,4 +279,47 @@ public function testCheckoutWithMainSubmit() { $this->assertEquals(new Price('899.10', 'USD'), $this->cart->getTotalPrice()); } + /** + * Tests that adding/removing coupons does not submit other panes. + */ + public function testCheckoutSubmit() { + // Start checkout, and enter billing information. + $this->drupalGet(Url::fromRoute('commerce_checkout.form', ['commerce_order' => $this->cart->id()])); + $this->getSession()->getPage()->findField('Example')->check(); + $this->waitForAjaxToFinish(); + $this->submitForm([ + 'payment_information[billing_information][address][0][address][given_name]' => 'Johnny', + 'payment_information[billing_information][address][0][address][family_name]' => 'Appleseed', + 'payment_information[billing_information][address][0][address][address_line1]' => '123 New York Drive', + 'payment_information[billing_information][address][0][address][locality]' => 'New York City', + 'payment_information[billing_information][address][0][address][administrative_area]' => 'NY', + 'payment_information[billing_information][address][0][address][postal_code]' => '10001', + ], 'Continue to review'); + + // Go back and edit the billing information, but don't submit it. + $this->getSession()->getPage()->clickLink('Go back'); + $address_prefix = 'payment_information[billing_information][address][0][address]'; + $this->getSession()->getPage()->fillField($address_prefix . '[given_name]', 'John'); + $this->getSession()->getPage()->fillField($address_prefix . '[family_name]', 'Smith'); + + // Add a coupon. + $coupons = $this->promotion->getCoupons(); + $coupon = reset($coupons); + $page = $this->getSession()->getPage(); + $page->fillField('Coupon code', $coupon->getCode()); + $page->pressButton('Apply coupon'); + $this->waitForAjaxToFinish(); + $this->assertSession()->pageTextContains($coupon->getCode()); + $this->assertSession()->fieldNotExists('Coupon code'); + $this->assertSession()->buttonNotExists('Apply coupon'); + + // Refresh the page and ensure the billing information hasn't been modified. + $this->drupalGet(Url::fromRoute('commerce_checkout.form', ['commerce_order' => $this->cart->id(), 'step' => 'order_information'])); + $page = $this->getSession()->getPage(); + $given_name_field = $page->findField('payment_information[billing_information][address][0][address][given_name]'); + $family_name_field = $page->findField('payment_information[billing_information][address][0][address][family_name]'); + $this->assertEquals($given_name_field->getValue(), 'Johnny'); + $this->assertEquals($family_name_field->getValue(), 'Appleseed'); + } + } diff --git a/src/Element/CommerceElementTrait.php b/src/Element/CommerceElementTrait.php index 74a6db4059..abf844fa91 100644 --- a/src/Element/CommerceElementTrait.php +++ b/src/Element/CommerceElementTrait.php @@ -2,6 +2,7 @@ namespace Drupal\commerce\Element; +use Drupal\Component\Utility\NestedArray; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Render\Element; @@ -71,21 +72,55 @@ public static function validateElementSubmit(array &$element, FormStateInterface * step of validation, allowing thrown exceptions to be converted into form * errors. * - * @param array $element - * The form element. + * @param array &$form + * The form. * @param \Drupal\Core\Form\FormStateInterface $form_state * The form state. */ - public static function executeElementSubmitHandlers(array &$element, FormStateInterface $form_state) { + public static function executeElementSubmitHandlers(array &$form, FormStateInterface $form_state) { if (!$form_state->isSubmitted() || $form_state->hasAnyErrors()) { // The form wasn't submitted (#ajax in progress) or failed validation. return; } + // A submit button might need to process only a part of the form. + // For example, the "Apply coupon" button at checkout should apply coupons, + // but not save the payment information. Use #limit_validation_errors + // as a guideline for which parts of the form to submit. + $triggering_element = $form_state->getTriggeringElement(); + if (isset($triggering_element['#limit_validation_errors']) && $triggering_element['#limit_validation_errors'] !== FALSE) { + // #limit_validation_errors => [], the button cares about nothing. + if (empty($triggering_element['#limit_validation_errors'])) { + return; + } + foreach ($triggering_element['#limit_validation_errors'] as $limit_validation_errors) { + $element = NestedArray::getValue($form, $limit_validation_errors); + if (!$element) { + // The element will be empty if #parents don't match #array_parents, + // the case for IEF widgets. In that case just submit everything. + $element = &$form; + } + self::doExecuteSubmitHandlers($element, $form_state); + } + } + else { + self::doExecuteSubmitHandlers($form, $form_state); + } + } + + /** + * Calls the #commerce_element_submit callbacks recursively. + * + * @param array &$element + * The current element. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The form state. + */ + public static function doExecuteSubmitHandlers(array &$element, FormStateInterface $form_state) { // Recurse through all children. foreach (Element::children($element) as $key) { if (!empty($element[$key])) { - static::executeElementSubmitHandlers($element[$key], $form_state); + static::doExecuteSubmitHandlers($element[$key], $form_state); } }