commit 3201dda9dec2c2b56c8b19bbc9a002c0155b93e9
Author: mglaman <mglaman@2416470.no-reply.drupal.org>
Date:   Sat Feb 27 00:46:24 2016 +0100

    Issue #2675836 by mglaman: Variation is not properly factoring attribute dependencies when selecting from user input

diff --git a/modules/product/src/Form/AddToCartForm.php b/modules/product/src/Form/AddToCartForm.php
index 365c8e4..c4c508f 100644
--- a/modules/product/src/Form/AddToCartForm.php
+++ b/modules/product/src/Form/AddToCartForm.php
@@ -241,7 +241,7 @@ class AddToCartForm extends FormBase {
   }
 
   /**
-   * Selects a product variation based on user input containing attribute values.
+   * Selects a product variation based on user input and attribute values.
    *
    * If there's no user input (form viewed for the first time), the default
    * variation is returned.
@@ -258,20 +258,24 @@ class AddToCartForm extends FormBase {
     $user_input = $form_state->getUserInput();
     $current_variation = reset($variations);
     if (!empty($user_input)) {
+      // Cascade through the dependent attributes, narrowing down the variation
+      // list. The last attribute defines the final available variations,
+      // so it's not used as a qualifying attribute.
       $attributes = $user_input['attributes'];
-      foreach ($variations as $variation) {
-        $match = TRUE;
-        foreach ($attributes as $field_name => $value) {
+      if (count($attributes) > 1) {
+        array_pop($attributes);
+      }
+
+      foreach ($attributes as $field_name => $value) {
+        foreach ($variations as $key => $variation) {
           if ($variation->get($field_name)->target_id != $value) {
-            $match = FALSE;
+            unset($variations[$key]);
           }
         }
-
-        if ($match) {
-          $current_variation = $variation;
-          break;
-        }
       }
+
+      // Select the first remaining variation.
+      $current_variation = reset($variations);
     }
 
     return $current_variation;
diff --git a/modules/product/src/Tests/AddToCartFormTest.php b/modules/product/src/Tests/AddToCartFormTest.php
index 20ab67f..42d3e64 100644
--- a/modules/product/src/Tests/AddToCartFormTest.php
+++ b/modules/product/src/Tests/AddToCartFormTest.php
@@ -7,10 +7,12 @@
 
 namespace Drupal\commerce_product\Tests;
 
+use Drupal\commerce_order\Entity\LineItemInterface;
 use Drupal\commerce_order\Entity\Order;
 use Drupal\commerce_order\Tests\OrderTestBase;
 use Drupal\commerce_product\Entity\ProductInterface;
 use Drupal\commerce_product\Entity\ProductVariation;
+use Drupal\commerce_product\Entity\ProductVariationInterface;
 use Drupal\commerce_product\Entity\ProductVariationType;
 use Drupal\commerce_product\Entity\ProductVariationTypeInterface;
 use Drupal\field\Entity\FieldConfig;
@@ -135,9 +137,10 @@ class AddToCartFormTest extends OrderTestBase {
     // Get the existing product page and submit Add to cart form.
     $this->postAddToCart($product);
     $this->assertEqual($variation3->{$attribute_name}->target_id, $attribute->id());
-    $ajax_commands = $this->drupalPostAjaxForm(NULL, [
+    $this->drupalPostAjaxForm(NULL, [
       'attributes[test_variation]' => $variation3->{$attribute_name}->target_id,
     ], 'attributes[test_variation]');
+    $this->assertAttributeExists('edit-attributes-test-variation', $variation3->{$attribute_name}->target_id);
     $this->postAddToCart($product, [
       'attributes[test_variation]' => $variation3->{$attribute_name}->target_id,
     ]);
@@ -147,11 +150,9 @@ class AddToCartFormTest extends OrderTestBase {
     $line_items = $this->cart->getLineItems();
     /** @var \Drupal\commerce_order\Entity\LineItemInterface $line_item */
     $line_item = $line_items[0];
-    $this->assertEqual($line_item->getTitle(), $this->variation->getLineItemTitle());
-    $this->assertTrue(($line_item->getQuantity() == 1), t('The product @product has been added to cart.', ['@product' => $line_item->getTitle()]));
+    $this->assertLineItemInOrder($this->variation, $line_item);
     $line_item = $line_items[1];
-    $this->assertEqual($line_item->getTitle(), $variation3->getLineItemTitle());
-    $this->assertTrue(($line_item->getQuantity() == 1), t('The product @product has been added to cart.', ['@product' => $line_item->getTitle()]));
+    $this->assertLineItemInOrder($variation3, $line_item);
   }
 
   /**
@@ -178,7 +179,7 @@ class AddToCartFormTest extends OrderTestBase {
     // Reload the variation since we have a new fields.
     $this->variation = ProductVariation::load($this->variation->id());
 
-    // Get the product so we can append new variations
+    // Get the product so we can append new variations.
     $product = $this->variation->getProduct();
 
     /**
@@ -226,26 +227,20 @@ class AddToCartFormTest extends OrderTestBase {
 
 
     $this->postAddToCart($this->variation->getProduct());
-    // Trigger AJAX by changing size attribute
-    $ajax_commands = $this->drupalPostAjaxForm(NULL, [
+
+    // Drill down to "medium" and "blue" variation.
+    $this->drupalPostAjaxForm(NULL, [
       'attributes[test_size_attribute]' => $size_attributes['medium']->id(),
     ], 'attributes[test_size_attribute]');
-    // Trigger AJAX by changing color attribute
-    $ajax_commands = $this->drupalPostAjaxForm(NULL, [
+    $this->assertAttributeExists('edit-attributes-test-color-attribute', $color_attributes['red']->id());
+    $this->assertAttributeExists('edit-attributes-test-color-attribute', $color_attributes['blue']->id());
+
+    $this->drupalPostAjaxForm(NULL, [
       'attributes[test_color_attribute]' => $color_attributes['blue']->id(),
     ], 'attributes[test_color_attribute]');
-
-    // We can't assert an option doesn't exist using AssertContentTrait, since
-    // our ID is dynamic. Version of assertNoOption using data-drupal-selector.
-    // @see \Drupal\simpletest\AssertContentTrait::assertNoOption
-    $selects = $this->xpath('//select[@data-drupal-selector=:data_drupal_selector]', [
-      ':data_drupal_selector' => 'edit-attributes-test-size-attribute',
-    ]);
-    $options = $this->xpath('//select[@data-drupal-selector=:data_drupal_selector]//option[@value=:option]', [
-      ':data_drupal_selector' => 'edit-attributes-test-size-attribute',
-      ':option' => $size_attributes['small']->id(),
-    ]);
-    $this->assertTrue(isset($selects[0]) && !isset($options[0]), NULL, 'Browser');
+    $this->assertAttributeExists('edit-attributes-test-color-attribute', $color_attributes['red']->id());
+    $this->assertAttributeExists('edit-attributes-test-color-attribute', $color_attributes['blue']->id());
+    $this->assertAttributeDoesNotExist('edit-attributes-test-size-attribute', $size_attributes['small']->id());
 
     $selects = $this->xpath('//select[@data-drupal-selector=:data_drupal_selector and @disabled]', [
       ':data_drupal_selector' => 'edit-attributes-test-size-attribute',
@@ -263,11 +258,9 @@ class AddToCartFormTest extends OrderTestBase {
     $line_items = $this->cart->getLineItems();
     /** @var \Drupal\commerce_order\Entity\LineItemInterface $line_item */
     $line_item = $line_items[0];
-    $this->assertEqual($line_item->getTitle(), $this->variation->getLineItemTitle());
-    $this->assertTrue(($line_item->getQuantity() == 1), t('The product @product has been added to cart.', ['@product' => $line_item->getTitle()]));
+    $this->assertLineItemInOrder($this->variation, $line_item);
     $line_item = $line_items[1];
-    $this->assertEqual($line_item->getTitle(), $variation3->getLineItemTitle());
-    $this->assertTrue(($line_item->getQuantity() == 1), t('The product @product has been added to cart.', ['@product' => $line_item->getTitle()]));
+    $this->assertLineItemInOrder($variation3, $line_item);
   }
 
   /**
@@ -317,17 +310,21 @@ class AddToCartFormTest extends OrderTestBase {
   /**
    * Creates an attribute.
    *
-   * @param string $bundle
+   * @param string $vocabulary
+   *   The vocabulary ID.
    * @param string $name
+   *   The attribute name.
    *
    * @return \Drupal\taxonomy\Entity\Term
+   *   The created attribute.
    */
-  protected function createAttributeOption($bundle, $name) {
+  protected function createAttributeOption($vocabulary, $name) {
     $attribute = $this->createEntity('taxonomy_term', [
-      'vid' => $bundle,
+      'vid' => $vocabulary,
       'name' => $name,
     ]);
     $attribute->save();
+
     return $attribute;
   }
 
@@ -335,9 +332,9 @@ class AddToCartFormTest extends OrderTestBase {
    * Posts the add to cart form for a product.
    *
    * @param \Drupal\commerce_product\Entity\ProductInterface $product
+   *   The product.
    * @param array $edit
-   *
-   * @throws \Exception
+   *   The values to post.
    */
   protected function postAddToCart(ProductInterface $product, array $edit = []) {
     $this->drupalGet('product/' . $product->id());
@@ -345,4 +342,54 @@ class AddToCartFormTest extends OrderTestBase {
     $this->drupalPostForm(NULL, $edit, t('Add to cart'));
   }
 
+  /**
+   * Asserts that an attribute option does exist.
+   *
+   * @param string $selector
+   *   The element selector.
+   * @param string $option
+   *   The option.
+   */
+  protected function assertAttributeExists($selector, $option) {
+    $options = $this->xpath('//select[@data-drupal-selector=:data_drupal_selector]//option[@value=:option]', [
+      ':data_drupal_selector' => $selector,
+      ':option' => $option,
+    ]);
+    $this->assertFalse(empty($options), 'The attribute is not available');
+  }
+
+  /**
+   * Asserts that an attribute option does not exist.
+   *
+   * @param string $selector
+   *   The element selector.
+   * @param string $option
+   *   The option.
+   */
+  protected function assertAttributeDoesNotExist($selector, $option) {
+    $options = $this->xpath('//select[@data-drupal-selector=:data_drupal_selector]//option[@value=:option]', [
+      ':data_drupal_selector' => $selector,
+      ':option' => $option,
+    ]);
+    $this->assertTrue(empty($options), 'The attribute is not available');
+  }
+
+  /**
+   * Assert the line item in the order is correct.
+   *
+   * @param \Drupal\commerce_product\Entity\ProductVariationInterface $variation
+   *   The purchased product variation.
+   * @param \Drupal\commerce_order\Entity\LineItemInterface $line_item
+   *   The line item.
+   * @param int $quantity
+   *   The quantity.
+   */
+  protected function assertLineItemInOrder(ProductVariationInterface $variation, LineItemInterface $line_item, $quantity = 1) {
+    $this->assertEqual($line_item->getTitle(), $variation->getLineItemTitle());
+    $this->assertTrue(($line_item->getQuantity() == $quantity), t('The product @product has been added to cart with quantity of @quantity.', [
+      '@product' => $line_item->getTitle(),
+      '@quantity' => $line_item->getQuantity(),
+    ]));
+  }
+
 }
