diff --git a/modules/checkout/src/Plugin/Commerce/CheckoutPane/BillingInformation.php b/modules/checkout/src/Plugin/Commerce/CheckoutPane/BillingInformation.php index e53b0f9..a24515f 100644 --- a/modules/checkout/src/Plugin/Commerce/CheckoutPane/BillingInformation.php +++ b/modules/checkout/src/Plugin/Commerce/CheckoutPane/BillingInformation.php @@ -33,18 +33,13 @@ class BillingInformation extends CheckoutPaneBase implements CheckoutPaneInterfa */ public function buildPaneForm(array $pane_form, FormStateInterface $form_state, array &$complete_form) { $store = $this->order->getStore(); - $billing_profile = $this->order->getBillingProfile(); - if (!$billing_profile) { - $profile_storage = $this->entityTypeManager->getStorage('profile'); - $billing_profile = $profile_storage->create([ - 'type' => 'customer', - 'uid' => $this->order->getCustomerId(), - ]); - } $pane_form['profile'] = [ '#type' => 'commerce_profile_select', - '#default_value' => $billing_profile, + '#title' => $this->t('Select an address'), + '#profile' => $this->getBillingProfile(), + '#profile_type' => 'customer', + '#profile_uid' => $this->order->getCustomerId(), '#default_country' => $store->getAddress()->getCountryCode(), '#available_countries' => $store->getBillingCountries(), ]; @@ -59,4 +54,40 @@ class BillingInformation extends CheckoutPaneBase implements CheckoutPaneInterfa $this->order->setBillingProfile($pane_form['profile']['#profile']); } + /** + * Gets the billing profile. + * + * @return \Drupal\profile\Entity\ProfileInterface + * The billing profile. + */ + protected function getBillingProfile() { + if ($billing_profile = $this->order->getBillingProfile()) { + return $billing_profile; + } + // If we still don't have a profile, assign the default profile with + // fallback to most recently created. + if (!$billing_profile && \Drupal::currentUser()->isAuthenticated()) { + $query = $this->entityTypeManager + ->getStorage('profile') + ->getQuery(); + $result = $query + ->condition('uid', \Drupal::currentUser()->id()) + ->sort('is_default') + ->sort('created', 'DESC') + ->execute(); + if ($result) { + $billing_profile = $this->entityTypeManager + ->getStorage('profile') + ->loadRevision(reset($result)); + } + } + if (!$billing_profile) { + $billing_profile = $this->entityTypeManager->getStorage('profile')->create([ + 'type' => 'customer', + 'uid' => $this->order->getCustomerId(), + ]); + } + return $billing_profile; + } + } diff --git a/modules/order/src/Element/ProfileSelect.php b/modules/order/src/Element/ProfileSelect.php index 6aacf1c..13ffc45 100644 --- a/modules/order/src/Element/ProfileSelect.php +++ b/modules/order/src/Element/ProfileSelect.php @@ -3,9 +3,12 @@ namespace Drupal\commerce_order\Element; use Drupal\commerce\Element\CommerceElementTrait; +use Drupal\Component\Utility\Html; +use Drupal\Component\Utility\NestedArray; use Drupal\Core\Entity\Entity\EntityFormDisplay; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Render\Element\RenderElement; +use Drupal\Core\Url; use Drupal\profile\Entity\ProfileInterface; /** @@ -15,7 +18,9 @@ use Drupal\profile\Entity\ProfileInterface; * @code * $form['billing_profile'] = [ * '#type' => 'commerce_profile_select', - * '#default_value' => $profile, + * '#profile' => $profile, + * '#profile_type' => 'customer', + * '#profile_uid' => \Drupal::currentUser()->id(), * '#default_country' => 'FR', * '#available_countries' => ['US', 'FR'], * ]; @@ -24,6 +29,10 @@ use Drupal\profile\Entity\ProfileInterface; * $form['billing_profile']['#profile']. Due to Drupal core limitations the * profile can't be accessed via $form_state->getValue('billing_profile'). * + * Note: + * This element always behaves as required. For optional behavior add + * a checkbox above the element that hides it on #ajax. + * * @RenderElement("commerce_profile_select") */ class ProfileSelect extends RenderElement { @@ -36,20 +45,31 @@ class ProfileSelect extends RenderElement { public function getInfo() { $class = get_class($this); return [ + '#tree' => TRUE, + '#title' => t('Select a profile'), + // The element doesn't use #default_value / #value. Use #profile instead. + '#default_value' => NULL, + // The preselected profile. A profile entity, or NULL. + '#profile' => NULL, + // Needed for creating new profiles, since #profile is not always given. + '#profile_type' => NULL, + '#profile_uid' => 0, + // Whether profiles should always be loaded in the latest revision. + // Disable when editing historical data, such as placed orders. + '#profile_latest_revision' => TRUE, // The country to select if the address widget doesn't have a default. '#default_country' => NULL, // A list of country codes. If empty, all countries will be available. '#available_countries' => [], + // Display options to administer profiles. + '#operation_buttons' => TRUE, - // The profile entity operated on. Required. - '#default_value' => NULL, '#process' => [ [$class, 'attachElementSubmit'], - [$class, 'processForm'], + [$class, 'processElement'], ], '#element_validate' => [ [$class, 'validateElementSubmit'], - [$class, 'validateForm'], ], '#commerce_element_submit' => [ [$class, 'submitForm'], @@ -59,28 +79,23 @@ class ProfileSelect extends RenderElement { } /** - * Builds the element form. + * Validates the element properties. * * @param array $element - * The form element being processed. - * @param \Drupal\Core\Form\FormStateInterface $form_state - * The current state of the form. - * @param array $complete_form - * The complete form structure. + * The form element. * * @throws \InvalidArgumentException - * Thrown when #default_value is empty or not an entity, or when - * #available_countries is not an array of country codes. - * - * @return array - * The processed form element. + * Thrown if an element property is invalid, or empty but required. */ - public static function processForm(array $element, FormStateInterface $form_state, array &$complete_form) { - if (empty($element['#default_value'])) { - throw new \InvalidArgumentException('The commerce_profile_select element requires the #default_value property.'); + public static function validateElementProperties(array $element) { + if (empty($element['#profile_type'])) { + throw new \InvalidArgumentException('The commerce_profile_select #profile_type property must be provided.'); } - elseif (isset($element['#default_value']) && !($element['#default_value'] instanceof ProfileInterface)) { - throw new \InvalidArgumentException('The commerce_profile_select #default_value property must be a profile entity.'); + if (isset($element['#profile']) && !($element['#profile'] instanceof ProfileInterface)) { + throw new \InvalidArgumentException('The commerce_profile_select #profile property must be a profile entity.'); + } + if (!empty($element['#profile_uid']) && !is_numeric($element['#profile_uid'])) { + throw new \InvalidArgumentException('The commerce_profile_select #profile_uid property must be a numeric entity ID.'); } if (!is_array($element['#available_countries'])) { throw new \InvalidArgumentException('The commerce_profile_select #available_countries property must be an array.'); @@ -91,8 +106,121 @@ class ProfileSelect extends RenderElement { $element['#default_country'] = NULL; } } + } + + /** + * Builds the element form. + * + * @param array $element + * The form element being processed. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + * + * @return array + * The processed form element. + */ + public static function processElement(array $element, FormStateInterface $form_state) { + self::validateElementProperties($element); + if (count(self::loadProfiles($element)) > 0) { + $element = self::addSelectList($element, $form_state); + if (!empty($element['#operation_buttons'])) { + $element = self::addModalLinks($element, $form_state); + } + } + else { + $element = self::addInlineForm($element, $form_state); + } + + return $element; + } + + /** + * Prepare options for select list and modal links. + * + * @param array $element + * The form element being processed. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + * + * @return array + * Array of element, profile options and selected option. + */ + protected static function prepareOptions(array $element, FormStateInterface $form_state) { + $element_state = self::getElementState($element['#parents'], $form_state); + // If the provided profile is in a previous revision, make sure it stays + // available as an option when $element['#profile'] gets changed. + if (empty($element['#profile_latest_revision']) && empty($element_state['fixed_option'])) { + if (!empty($element['#profile']) && !$element['#profile']->isDefaultRevision()) { + $element_state['fixed_option'] = [ + 'id' => self::buildOptionId($element['#profile'], $element['#profile_latest_revision']), + 'label' => $element['#profile']->label() . ' ' . t('(Original)'), + ]; + } + } + $profiles = self::loadProfiles($element); + $profile_options = []; + foreach ($profiles as $profile) { + $option_id = self::buildOptionId($profile, $element['#profile_latest_revision']); + $profile_options[$option_id] = $profile->label(); + } + $user_input = $form_state->getUserInput(); + $selected_option = NestedArray::getValue($user_input, array_merge($element['#parents'], ['profile_selection'])); + if (!empty($selected_option)) { + // Keep the last selection in element state, so that the element knows + // where to return the customer upon cancelling form mode. + $element_state['selected_option'] = $selected_option; + } + elseif (!empty($element_state['selected_option'])) { + $selected_option = $element_state['selected_option']; + } + elseif ($default_profile = self::selectDefaultProfile($element, $profiles)) { + $selected_option = self::buildOptionId($default_profile, $element['#profile_latest_revision']); + } + /** @var \Drupal\profile\ProfileStorageInterface $profile_storage */ + $profile_storage = \Drupal::entityTypeManager()->getStorage('profile'); + $id_parts = explode('-', $selected_option); + if (!empty($id_parts[1])) { + $element['#profile'] = $profile_storage->loadRevision($id_parts[1]); + } + elseif (!empty($id_parts[0])) { + $element['#profile'] = $profile_storage->load($id_parts[0]); + } + else { + $element['#profile'] = $profile_storage->create([ + 'type' => $element['#profile_type'], + 'uid' => $element['#profile_uid'], + ]); + } + // Persist any changes made to $element_state. + self::setElementState($element['#parents'], $form_state, $element_state); + + return [ + $element, + $profile_options, + $selected_option, + ]; + } - $element['#profile'] = $element['#default_value']; + /** + * Provide render array for an inline form. + * + * @param array $element + * The form element being processed. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + * + * @return array + * Render array for an inline form. + */ + protected static function addInlineForm(array $element, FormStateInterface $form_state) { + /** @var \Drupal\profile\ProfileStorageInterface $profile_storage */ + $profile_storage = \Drupal::entityTypeManager()->getStorage('profile'); + if (empty($element['#profile'])) { + $element['#profile'] = $profile_storage->create([ + 'type' => $element['#profile_type'], + 'uid' => $element['#profile_uid'], + ]); + } $form_display = EntityFormDisplay::collectRenderDisplay($element['#profile'], 'default'); $form_display->buildForm($element['#profile'], $element, $form_state); if (!empty($element['address']['widget'][0])) { @@ -108,26 +236,120 @@ class ProfileSelect extends RenderElement { $widget_element['address']['#available_countries'] = $element['#available_countries']; } } + return $element; + } + + /** + * Provide render array for select list. + * + * @param array $element + * The form element being processed. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + * + * @return array + * Render array for an inline form. + */ + protected static function addSelectList(array $element, FormStateInterface $form_state) { + list($element, $profile_options, $selected_option) = self::prepareOptions($element, $form_state); + $id_prefix = implode('-', $element['#parents']); + $wrapper_id = Html::getUniqueId("$id_prefix-ajax-wrapper"); + // Pass the id along to other methods. + $element['#wrapper_id'] = $wrapper_id; + $view_builder = \Drupal::entityTypeManager()->getViewBuilder('profile'); + $element['profile_selection'] = [ + '#type' => 'select', + '#title' => $element['#title'], + '#options' => $profile_options, + '#default_value' => $selected_option, + '#ajax' => [ + 'callback' => [get_called_class(), 'ajaxRefresh'], + 'wrapper' => $wrapper_id, + ], + '#limit_validation_errors' => [], + '#weight' => -1, + '#access' => !empty($profile_options), + ]; + $element['profile'] = $view_builder->view($element['#profile'], 'default'); + $element['#prefix'] = "
"; + $element['#suffix'] = '
'; return $element; } /** - * Validates the element form. + * Provide render array for modal links. * * @param array $element - * The form element. + * The form element being processed. * @param \Drupal\Core\Form\FormStateInterface $form_state * The current state of the form. * - * @throws \Exception - * Thrown if button-level #validate handlers are detected on the parent - * form, as a protection against buggy behavior. + * @return array + * Render array for an inline form. */ - public static function validateForm(array &$element, FormStateInterface $form_state) { - $form_display = EntityFormDisplay::collectRenderDisplay($element['#profile'], 'default'); - $form_display->extractFormValues($element['#profile'], $element, $form_state); - $form_display->validateFormValues($element['#profile'], $element, $form_state); + protected static function addModalLinks(array $element, FormStateInterface $form_state) { + $url = Url::fromRoute('entity.profile.type.user_profile_form.add', ['user' => $element['#profile']->getOwnerId(), 'profile_type' => $element['#profile_type']]); + $url->mergeOptions(['query' => ['destination' => \Drupal::service('path.current')->getPath()]]); + $element['new'] = [ + '#type' => 'link', + '#url' => $url, + '#title' => t('Add new profile'), + '#attributes' => [ + 'class' => [ + 'use-ajax', + 'button', + ], + 'data-dialog-type' => 'modal', + 'data-dialog-options' => json_encode([ + 'width' => 700, + ]), + ], + ]; + if (!$element['#profile']->isNew()) { + $url = Url::fromRoute('entity.profile.edit_form', ['profile' => $element['#profile']->id()]); + $url->mergeOptions(['query' => ['destination' => \Drupal::service('path.current')->getPath()]]); + $element['edit'] = [ + '#type' => 'link', + '#url' => $url, + '#title' => t('Edit profile'), + '#attributes' => [ + 'class' => [ + 'use-ajax', + 'button', + ], + 'data-dialog-type' => 'modal', + 'data-dialog-options' => json_encode([ + 'width' => 700, + ]), + ], + ]; + } + $element['#attached']['library'][] = 'core/drupal.dialog.ajax'; + + return $element; + } + + /** + * Loads related profiles. + * + * @param array $element + * The form element being processed. + * + * @return \Drupal\profile\ProfileStorageInterface[] + * A list of loaded profiles. + */ + protected static function loadProfiles(array $element) { + /** @var \Drupal\profile\ProfileStorageInterface $profile_storage */ + $profile_storage = \Drupal::entityTypeManager()->getStorage('profile'); + /** @var \Drupal\user\UserStorageInterface $user_storage */ + $user_storage = \Drupal::entityTypeManager()->getStorage('user'); + $user = $element['#profile_uid'] ? $user_storage->load($element['#profile_uid']) : NULL; + $profiles = []; + if ($user) { + $profiles = $profile_storage->loadMultipleByUser($user, $element['#profile_type'], TRUE); + } + return $profiles; } /** @@ -144,4 +366,92 @@ class ProfileSelect extends RenderElement { $element['#profile']->save(); } + /** + * Ajax callback. + */ + public static function ajaxRefresh(array &$form, FormStateInterface $form_state) { + $triggering_element = $form_state->getTriggeringElement(); + $element = NestedArray::getValue($form, array_slice($triggering_element['#array_parents'], 0, -1)); + return $element; + } + + /** + * Builds the option ID for the given profile. + * + * @param \Drupal\profile\Entity\ProfileInterface $profile + * The profile. + * @param bool $use_latest_revision + * (optional) Use the latest revision, defaults to TRUE. + * + * @return string + * The option ID. + */ + public static function buildOptionId(ProfileInterface $profile, $use_latest_revision = TRUE) { + $option_id = $profile->id(); + if (!$use_latest_revision) { + $option_id .= '-' . $profile->getRevisionId(); + } + return $option_id; + } + + /** + * Selects the default profile for the given element. + * + * @param array $element + * The element. + * @param array $profiles + * The available profiles. + * + * @return \Drupal\profile\Entity\ProfileInterface|null + * The default profile, or NULL if none found. + */ + public static function selectDefaultProfile(array $element, array $profiles) { + $default_profile = NULL; + if (!empty($element['#profile'])) { + $default_profile = $element['#profile']; + } + elseif (!empty($profiles)) { + $default_profile = reset($profiles); + foreach ($profiles as $profile) { + if ($profile->isDefault()) { + $default_profile = $profile; + break; + } + } + } + return $default_profile; + } + + /** + * Sets the element state. + * + * @param array $parents + * The element parents. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + * @param array $element_state + * The element state. + */ + public static function setElementState(array $parents, FormStateInterface $form_state, array $element_state) { + $parents = array_merge(['element_state', '#parents'], $parents); + NestedArray::setValue($form_state->getStorage(), $parents, $element_state); + } + + /** + * Gets the element state. + * + * @param array $parents + * The element parents. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + * + * @return array + * The element state. + */ + public static function getElementState(array $parents, FormStateInterface $form_state) { + $parents = array_merge(['element_state', '#parents'], $parents); + $element_state = (array) NestedArray::getValue($form_state->getStorage(), $parents); + return $element_state; + } + } diff --git a/modules/order/src/Plugin/Field/FieldWidget/BillingProfileWidget.php b/modules/order/src/Plugin/Field/FieldWidget/BillingProfileWidget.php index 2d0987c..fecdcc8 100644 --- a/modules/order/src/Plugin/Field/FieldWidget/BillingProfileWidget.php +++ b/modules/order/src/Plugin/Field/FieldWidget/BillingProfileWidget.php @@ -3,13 +3,10 @@ namespace Drupal\commerce_order\Plugin\Field\FieldWidget; use Drupal\Component\Utility\NestedArray; -use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Field\FieldItemListInterface; use Drupal\Core\Field\WidgetBase; use Drupal\Core\Form\FormStateInterface; -use Drupal\Core\Plugin\ContainerFactoryPluginInterface; -use Symfony\Component\DependencyInjection\ContainerInterface; /** * Plugin implementation of 'commerce_billing_profile'. @@ -22,50 +19,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface; * } * ) */ -class BillingProfileWidget extends WidgetBase implements ContainerFactoryPluginInterface { - - /** - * The entity type manager. - * - * @var \Drupal\Core\Entity\EntityTypeManagerInterface - */ - protected $entityTypeManager; - - /** - * Constructs a new BillingProfileWidget object. - * - * @param string $plugin_id - * The plugin_id for the widget. - * @param mixed $plugin_definition - * The plugin implementation definition. - * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition - * The definition of the field to which the widget is associated. - * @param array $settings - * The widget settings. - * @param array $third_party_settings - * Any third party settings. - * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager - * The entity type manager. - */ - public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, EntityTypeManagerInterface $entity_type_manager) { - parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings); - - $this->entityTypeManager = $entity_type_manager; - } - - /** - * {@inheritdoc} - */ - public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { - return new static( - $plugin_id, - $plugin_definition, - $configuration['field_definition'], - $configuration['settings'], - $configuration['third_party_settings'], - $container->get('entity_type.manager') - ); - } +class BillingProfileWidget extends WidgetBase { /** * {@inheritdoc} @@ -75,20 +29,14 @@ class BillingProfileWidget extends WidgetBase implements ContainerFactoryPluginI $order = $items[$delta]->getEntity(); $store = $order->getStore(); - if (!$items[$delta]->isEmpty()) { - $profile = $items[$delta]->entity; - } - else { - $profile = $this->entityTypeManager->getStorage('profile')->create([ - 'type' => 'customer', - 'uid' => $order->getCustomerId(), - ]); - } - $element['#type'] = 'fieldset'; $element['profile'] = [ '#type' => 'commerce_profile_select', - '#default_value' => $profile, + '#title' => $this->t('Select an address'), + '#profile' => $items[$delta]->entity, + '#profile_type' => 'customer', + '#profile_uid' => $order->getCustomerId(), + '#profile_latest_revision' => $order->getState()->value == 'draft', '#default_country' => $store->getAddress()->getCountryCode(), '#available_countries' => $store->getBillingCountries(), ]; diff --git a/modules/order/tests/modules/commerce_order_test/commerce_order_test.routing.yml b/modules/order/tests/modules/commerce_order_test/commerce_order_test.routing.yml new file mode 100644 index 0000000..466dd4c --- /dev/null +++ b/modules/order/tests/modules/commerce_order_test/commerce_order_test.routing.yml @@ -0,0 +1,9 @@ +commerce_order_test.profile_select_form: + path: '/commerce_order_test/profile_select_test_form' + defaults: + _form: '\Drupal\commerce_order_test\Form\ProfileSelectTestForm' + _title: 'Profile select test form' + requirements: + _access: 'TRUE' + options: + no_cache: TRUE diff --git a/modules/order/tests/modules/commerce_order_test/src/Form/ProfileSelectTestForm.php b/modules/order/tests/modules/commerce_order_test/src/Form/ProfileSelectTestForm.php new file mode 100644 index 0000000..1c59cb8 --- /dev/null +++ b/modules/order/tests/modules/commerce_order_test/src/Form/ProfileSelectTestForm.php @@ -0,0 +1,43 @@ + 'commerce_profile_select', + '#profile_type' => 'customer', + '#profile_uid' => \Drupal::currentUser()->id(), + '#available_countries' => ['FR', 'DE', 'HU', 'RS', 'US'], + ]; + $form['submit'] = [ + '#type' => 'submit', + '#value' => $this->t('Submit'), + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $profile = $form['profile']['#profile']; + drupal_set_message($this->t('Profile selected: :label', [':label' => $profile->label()])); + } + +} diff --git a/modules/order/tests/src/Functional/OrderAdminTest.php b/modules/order/tests/src/Functional/OrderAdminTest.php index 9476000..104a404 100644 --- a/modules/order/tests/src/Functional/OrderAdminTest.php +++ b/modules/order/tests/src/Functional/OrderAdminTest.php @@ -6,6 +6,7 @@ use Drupal\commerce_order\Adjustment; use Drupal\commerce_order\Entity\Order; use Drupal\commerce_price\Price; use Drupal\profile\Entity\Profile; +use Drupal\Tests\commerce\FunctionalJavascript\JavascriptTestTrait; /** * Tests the commerce_order entity forms. @@ -14,28 +15,14 @@ use Drupal\profile\Entity\Profile; */ class OrderAdminTest extends OrderBrowserTestBase { - /** - * The profile to test against. - * - * @var \Drupal\profile\Entity\Profile - */ - protected $billingProfile; + use JavascriptTestTrait; /** * {@inheritdoc} */ - protected function setUp() { - parent::setUp(); - \Drupal::service('module_installer')->install(['profile']); - - $profile_values = [ - 'type' => 'customer', - 'uid' => 1, - 'status' => 1, - ]; - $this->billingProfile = Profile::create($profile_values); - $this->billingProfile->save(); - } + public static $modules = [ + 'profile', + ]; /** * Tests creating/editing an Order. @@ -43,6 +30,9 @@ class OrderAdminTest extends OrderBrowserTestBase { * @group failing */ public function testCreateOrder() { + $assert = $this->assertSession(); + $page = $this->getSession()->getPage(); + // Create an order through the add form. $this->drupalGet('/admin/commerce/orders'); $this->getSession()->getPage()->clickLink('Create a new order'); @@ -55,7 +45,8 @@ class OrderAdminTest extends OrderBrowserTestBase { // Check the integrity of the edit form. $this->assertSession()->statusCodeEquals(200); - $this->assertSession()->fieldExists('billing_profile[0][profile][address][0][address][given_name]'); + $input = $page->find('named', ['id_or_name', 'profile[address][0][address][country_code]']); + $this->assertNotEmpty($input, sprintf('%s was not found', 'country_code')); $this->assertSession()->fieldExists('order_items[form][inline_entity_form][purchased_entity][0][target_id]'); $this->assertSession()->fieldExists('order_items[form][inline_entity_form][quantity][0][value]'); $this->assertSession()->fieldExists('order_items[form][inline_entity_form][unit_price][0][amount][number]'); @@ -91,16 +82,26 @@ class OrderAdminTest extends OrderBrowserTestBase { 'order_items[form][inline_entity_form][entities][0][form][unit_price][0][amount][number]' => '1.11', ]; $this->submitForm($edit, 'Update order item'); - $edit = [ - 'billing_profile[0][profile][address][0][address][given_name]' => 'John', - 'billing_profile[0][profile][address][0][address][family_name]' => 'Smith', - 'billing_profile[0][profile][address][0][address][address_line1]' => '123 street', - 'billing_profile[0][profile][address][0][address][postal_code]' => '94043', - 'billing_profile[0][profile][address][0][address][locality]' => 'Mountain View', - 'billing_profile[0][profile][address][0][address][administrative_area]' => 'CA', - ]; + // There is no adjustment - the order should save successfully. - $this->submitForm($edit, 'Save'); + $input = $page->find('named', ['id_or_name', 'profile[address][0][address][country_code]']); + $this->assertNotEmpty($input, sprintf('%s was not found', 'country_code')); + $input->setValue('US'); + $this->waitForAjaxToFinish(); + $address = [ + 'postal_code' => '53177', + 'locality' => 'Milwaukee', + 'address_line1' => 'Pabst Blue Ribbon Dr', + 'administrative_area' => 'WI', + 'given_name' => 'Frederick', + 'family_name' => 'Pabst', + ]; + foreach ($address as $key => $value) { + $input = $page->find('named', ['id_or_name', "profile[address][0][address][$key]"]); + $this->assertNotEmpty($input, sprintf('%s was not found', $key)); + $input->setValue($value); + } + $this->submitForm([], 'Save'); $this->assertSession()->pageTextContains('The order has been successfully saved.'); // Use an adjustment that is not locked by default. diff --git a/modules/order/tests/src/FunctionalJavascript/ProfileSelectTest.php b/modules/order/tests/src/FunctionalJavascript/ProfileSelectTest.php new file mode 100644 index 0000000..5daf2a0 --- /dev/null +++ b/modules/order/tests/src/FunctionalJavascript/ProfileSelectTest.php @@ -0,0 +1,337 @@ + 'HU', + 'given_name' => 'Gustav', + 'family_name' => 'Mahler', + 'address_line1' => 'Teréz körút 7', + 'locality' => 'Budapest', + 'postal_code' => '1067', + ]; + + /** + * Profile address values. + * + * @var array + */ + protected $address2 = [ + 'country_code' => 'DE', + 'given_name' => 'Johann Sebastian', + 'family_name' => 'Bach', + 'address_line1' => 'Thomaskirchhof 15', + 'locality' => 'Leipzig', + 'postal_code' => '04109', + ]; + + /** + * The profile storage. + * + * @var \Drupal\profile\ProfileStorageInterface + */ + protected $profileStorage; + + /** + * {@inheritdoc} + */ + public static $modules = [ + 'commerce_order', + 'commerce_order_test', + ]; + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + + $this->profileStorage = $this->container->get('entity_type.manager')->getStorage('profile'); + } + + /** + * {@inheritdoc} + */ + protected function getAdministratorPermissions() { + return [ + 'view own customer profile', + 'update own customer profile', + ] + parent::getAdministratorPermissions(); + } + + /** + * Tests creating a profile as an authenticated user. + */ + public function testAnonymous() { + $page = $this->getSession()->getPage(); + $assert = $this->assertSession(); + + // Create a profile for a different user to make sure it's not shown. + $profile1 = $this->createEntity('profile', [ + 'type' => 'customer', + 'uid' => $this->adminUser->id(), + 'address' => $this->address1, + ]); + + $this->drupalLogout(); + $this->drupalGet(Url::fromRoute('commerce_order_test.profile_select_form')); + $assert->pageTextContains('Profile select test form'); + $assert->pageTextNotContains('Select a profile'); + $input = $page->find('named', ['id_or_name', 'profile[address][0][address][country_code]']); + $this->assertNotEmpty($input, sprintf('%s was not found', 'country_code')); + $input->setValue($this->address1['country_code']); + $assert->assertWaitOnAjaxRequest(); + foreach ($this->address1 as $key => $value) { + if ($key == 'country_code') { + continue; + } + $input = $page->find('named', ['id_or_name', "profile[address][0][address][$key]"]); + $this->assertNotEmpty($input, sprintf('%s was not found', $key)); + $input->setValue($value); + } + $this->submitForm([], 'Submit'); + /** @var \Drupal\profile\Entity\ProfileInterface $profile */ + $profile = $this->profileStorage->load(1); + $assert->responseContains(sprintf('Profile selected: %s', $profile->label())); + + /** @var \Drupal\address\Plugin\Field\FieldType\AddressItem $address */ + $address = $profile->get('address')->first(); + $this->assertEquals($this->address1['country_code'], $address->getCountryCode()); + $this->assertEquals($this->address1['given_name'], $address->getGivenName()); + $this->assertEquals($this->address1['family_name'], $address->getFamilyName()); + $this->assertEquals($this->address1['address_line1'], $address->getAddressLine1()); + $this->assertEquals($this->address1['locality'], $address->getLocality()); + $this->assertEquals($this->address1['postal_code'], $address->getPostalCode()); + } + + /** + * Tests selecting a profile as an authenticated user. + */ + public function testAuthenticated() { + $page = $this->getSession()->getPage(); + $assert = $this->assertSession(); + + /** @var \Drupal\profile\Entity\ProfileInterface $profile1 */ + $profile1 = $this->createEntity('profile', [ + 'type' => 'customer', + 'uid' => $this->adminUser->id(), + 'address' => $this->address1, + ]); + $this->createEntity('profile', [ + 'type' => 'customer', + 'uid' => $this->adminUser->id(), + 'address' => $this->address2, + 'is_default' => TRUE, + ]); + + $this->drupalGet(Url::fromRoute('commerce_order_test.profile_select_form')); + $assert->pageTextContains('Profile select test form'); + $assert->fieldExists('Select a profile'); + // The last created profile should be selected by default. + $assert->pageTextContains($this->address2['locality']); + + $page->fillField('Select a profile', $profile1->id()); + $assert->assertWaitOnAjaxRequest(); + $assert->pageTextContains($this->address1['locality']); + $this->submitForm([], 'Submit'); + $assert->responseContains(sprintf('Profile selected: %s', $profile1->label())); + + $this->profileStorage->resetCache([$profile1->id()]); + $profile1 = $this->profileStorage->load($profile1->id()); + /** @var \Drupal\address\Plugin\Field\FieldType\AddressItem $address */ + $address = $profile1->get('address')->first(); + // Assert that field values have not changed. + $this->assertEquals($this->address1['country_code'], $address->getCountryCode()); + $this->assertEquals($this->address1['given_name'], $address->getGivenName()); + $this->assertEquals($this->address1['family_name'], $address->getFamilyName()); + $this->assertEquals($this->address1['address_line1'], $address->getAddressLine1()); + $this->assertEquals($this->address1['locality'], $address->getLocality()); + $this->assertEquals($this->address1['postal_code'], $address->getPostalCode()); + + $profiles = $this->profileStorage->loadMultipleByUser($this->adminUser, 'customer', TRUE); + $this->assertCount(2, $profiles); + } + + /** + * Tests creating the initial profile as an authenticated user. + */ + public function testAuthenticatedCreate() { + $page = $this->getSession()->getPage(); + $assert = $this->assertSession(); + + $this->drupalGet(Url::fromRoute('commerce_order_test.profile_select_form')); + $assert->pageTextContains('Profile select test form'); + $assert->pageTextNotContains('Select a profile'); + $page->fillField('profile[address][0][address][country_code]', $this->address1['country_code']); + $assert->assertWaitOnAjaxRequest(); + + foreach ($this->address1 as $key => $value) { + if ($key == 'country_code') { + continue; + } + $input = $page->find('named', ['id_or_name', "profile[address][0][address][$key]"]); + $this->assertNotEmpty($input, sprintf('%s was not found', $key)); + $input->setValue($value); + } + $this->submitForm([], 'Submit'); + + /** @var \Drupal\profile\Entity\ProfileInterface $profile1 */ + $profile1 = $this->profileStorage->load(1); + $assert->responseContains(sprintf('Profile selected: %s', $profile1->label())); + + /** @var \Drupal\address\Plugin\Field\FieldType\AddressItem $address1 */ + $address1 = $profile1->get('address')->first(); + $this->assertEquals($this->address1['country_code'], $address1->getCountryCode()); + $this->assertEquals($this->address1['given_name'], $address1->getGivenName()); + $this->assertEquals($this->address1['family_name'], $address1->getFamilyName()); + $this->assertEquals($this->address1['address_line1'], $address1->getAddressLine1()); + $this->assertEquals($this->address1['locality'], $address1->getLocality()); + $this->assertEquals($this->address1['postal_code'], $address1->getPostalCode()); + + $profiles = $this->profileStorage->loadMultipleByUser($this->adminUser, 'customer', TRUE); + $this->assertCount(1, $profiles); + + $this->clickLink('Add new profile'); + $this->assertNotEmpty($assert->waitForElementVisible('css', '.ui-dialog')); + $assert->elementContains('css', 'span.ui-dialog-title', 'Create Customer'); + $input = $page->find('named', ['id_or_name', 'address[0][address][country_code]']); + $this->assertNotEmpty($input, sprintf('%s was not found', 'country_code')); + $input->setValue($this->address2['country_code']); + $this->waitForAjaxToFinish(); + foreach ($this->address2 as $key => $value) { + if ($key == 'country_code') { + continue; + } + $input = $page->find('named', ['id_or_name', "address[0][address][$key]"]); + $this->assertNotEmpty($input, sprintf('%s was not found', $key)); + $input->setValue($value); + } + $this->clickModalSubmit('Save and make default'); + $this->submitForm([], 'Submit'); + + /** @var \Drupal\profile\Entity\ProfileInterface $profile */ + $profile2 = $this->profileStorage->load(2); + $assert->responseContains(sprintf('Profile selected: %s', $profile2->label())); + + /** @var \Drupal\address\Plugin\Field\FieldType\AddressItem $address2 */ + $address2 = $profile2->get('address')->first(); + $this->assertEquals($this->address2['country_code'], $address2->getCountryCode()); + $this->assertEquals($this->address2['given_name'], $address2->getGivenName()); + $this->assertEquals($this->address2['family_name'], $address2->getFamilyName()); + $this->assertEquals($this->address2['address_line1'], $address2->getAddressLine1()); + $this->assertEquals($this->address2['locality'], $address2->getLocality()); + $this->assertEquals($this->address2['postal_code'], $address2->getPostalCode()); + + $profiles = $this->profileStorage->loadMultipleByUser($this->adminUser, 'customer', TRUE); + $this->assertCount(2, $profiles); + } + + /** + * Tests editing a profile as an authenticated user. + * + * @group debug + */ + public function testAuthenticatedEdit() { + $page = $this->getSession()->getPage(); + $assert = $this->assertSession(); + + /** @var \Drupal\profile\Entity\ProfileInterface $profile1 */ + $profile1 = $this->createEntity('profile', [ + 'type' => 'customer', + 'uid' => $this->adminUser->id(), + 'address' => $this->address1, + ]); + $profile1->save(); + /** @var \Drupal\profile\Entity\ProfileInterface $profile2 */ + $profile2 = $this->createEntity('profile', [ + 'type' => 'customer', + 'uid' => $this->adminUser->id(), + 'address' => $this->address2, + 'is_default' => TRUE, + ]); + $profile2->save(); + + $this->drupalGet(Url::fromRoute('commerce_order_test.profile_select_form')); + $assert->pageTextContains('Profile select test form'); + $assert->fieldExists('Select a profile'); + // The last created profile should be selected by default. + $assert->pageTextContains($this->address2['locality']); + $this->clickLink('Edit profile'); + + $this->assertNotEmpty($assert->waitForElementVisible('css', '.ui-dialog')); + $assert->elementContains('css', 'span.ui-dialog-title', sprintf('Edit %s', $this->address2['address_line1'])); + foreach ($this->address2 as $key => $value) { + $input = $page->find('named', ['id_or_name', "address[0][address][$key]"]); + $this->assertEquals($value, $input->getValue()); + } + $input = $page->find('named', ['id_or_name', 'address[0][address][address_line1]']); + $input->setValue('Andrássy út 22'); + $this->clickModalSubmit('Save'); + $this->submitForm([], 'Submit'); + + $this->profileStorage->resetCache([$profile2->id()]); + $profile2 = $this->profileStorage->load($profile2->id()); + /** @var \Drupal\address\Plugin\Field\FieldType\AddressItem $address */ + $address = $profile2->get('address')->first(); + $assert->responseContains(sprintf('Profile selected: %s', $profile2->label())); + // Assert that all but street1 field values have not changed. + $this->assertEquals($this->address2['country_code'], $address->getCountryCode()); + $this->assertEquals($this->address2['given_name'], $address->getGivenName()); + $this->assertEquals($this->address2['family_name'], $address->getFamilyName()); + $this->assertEquals('Andrássy út 22', $address->getAddressLine1()); + $this->assertEquals($this->address2['locality'], $address->getLocality()); + $this->assertEquals($this->address2['postal_code'], $address->getPostalCode()); + + $profiles = $this->profileStorage->loadMultipleByUser($this->adminUser, 'customer', TRUE); + $this->assertCount(2, $profiles); + } + + /** + * Click a modal submit button. + * + * @param string $text + * The button text. + * + * @todo: Switch to using NodeElement::click() on the button or + * NodeElement::submit() on the form when #2831506 is fixed. + * + * @see https://www.drupal.org/node/2831506 + */ + protected function clickModalSubmit($text) { + $this->waitForAjaxToFinish(); + $condition = <<getSession()->wait(10000, $condition); + $condition = <<getSession()->wait(10000, $condition); + } + +} diff --git a/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentInformation.php b/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentInformation.php index a91aabe..92be4b9 100644 --- a/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentInformation.php +++ b/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentInformation.php @@ -260,17 +260,13 @@ class PaymentInformation extends CheckoutPaneBase { */ protected function buildBillingProfileForm(array $pane_form, FormStateInterface $form_state) { $store = $this->order->getStore(); - $billing_profile = $this->order->getBillingProfile(); - if (!$billing_profile) { - $billing_profile = $this->entityTypeManager->getStorage('profile')->create([ - 'uid' => $this->order->getCustomerId(), - 'type' => 'customer', - ]); - } $pane_form['billing_information'] = [ '#type' => 'commerce_profile_select', - '#default_value' => $billing_profile, + '#title' => $this->t('Select an address'), + '#profile' => $this->order->getBillingProfile(), + '#profile_type' => 'customer', + '#profile_uid' => $this->order->getCustomerId(), '#default_country' => $store->getAddress()->getCountryCode(), '#available_countries' => $store->getBillingCountries(), ]; diff --git a/modules/payment/src/PluginForm/PaymentMethodAddForm.php b/modules/payment/src/PluginForm/PaymentMethodAddForm.php index 44c6c69..616d6a4 100644 --- a/modules/payment/src/PluginForm/PaymentMethodAddForm.php +++ b/modules/payment/src/PluginForm/PaymentMethodAddForm.php @@ -6,7 +6,6 @@ use Drupal\commerce_payment\CreditCard; use Drupal\commerce_payment\Exception\DeclineException; use Drupal\commerce_payment\Exception\PaymentGatewayException; use Drupal\Core\Form\FormStateInterface; -use Drupal\profile\Entity\Profile; class PaymentMethodAddForm extends PaymentGatewayFormBase { @@ -54,16 +53,6 @@ class PaymentMethodAddForm extends PaymentGatewayFormBase { /** @var \Drupal\commerce_payment\Entity\PaymentMethodInterface $payment_method */ $payment_method = $this->entity; - /** @var \Drupal\profile\Entity\ProfileInterface $billing_profile */ - $billing_profile = $payment_method->getBillingProfile(); - if (!$billing_profile) { - /** @var \Drupal\profile\Entity\ProfileInterface $billing_profile */ - $billing_profile = Profile::create([ - 'type' => 'customer', - 'uid' => $payment_method->getOwnerId(), - ]); - } - if ($order = $this->routeMatch->getParameter('commerce_order')) { $store = $order->getStore(); } @@ -76,7 +65,9 @@ class PaymentMethodAddForm extends PaymentGatewayFormBase { $form['billing_information'] = [ '#parents' => array_merge($form['#parents'], ['billing_information']), '#type' => 'commerce_profile_select', - '#default_value' => $billing_profile, + '#title' => t('Select an address'), + '#profile_type' => 'customer', + '#profile_uid' => $payment_method->getOwnerId(), '#default_country' => $store ? $store->getAddress()->getCountryCode() : NULL, '#available_countries' => $store ? $store->getBillingCountries() : [], ]; diff --git a/modules/payment/tests/src/FunctionalJavascript/PaymentCheckoutTest.php b/modules/payment/tests/src/FunctionalJavascript/PaymentCheckoutTest.php index 099b70e..ce9b2f2 100644 --- a/modules/payment/tests/src/FunctionalJavascript/PaymentCheckoutTest.php +++ b/modules/payment/tests/src/FunctionalJavascript/PaymentCheckoutTest.php @@ -34,6 +34,13 @@ class PaymentCheckoutTest extends CommerceBrowserTestBase { protected $product; /** + * The profile. + * + * @var \Drupal\profile\Entity\ProfileInterface + */ + protected $adminProfile; + + /** * A non-reusable order payment method. * * @var \Drupal\commerce_payment\Entity\PaymentMethodInterface @@ -148,7 +155,7 @@ class PaymentCheckoutTest extends CommerceBrowserTestBase { ]); $gateway->save(); - $profile = $this->createEntity('profile', [ + $this->adminProfile = $this->createEntity('profile', [ 'type' => 'customer', 'address' => [ 'country_code' => 'US', @@ -167,11 +174,11 @@ class PaymentCheckoutTest extends CommerceBrowserTestBase { 'payment_gateway' => 'onsite', 'card_type' => 'visa', 'card_number' => '1111', - 'billing_profile' => $profile, + 'billing_profile' => $this->adminProfile, 'reusable' => TRUE, 'expires' => strtotime('2028/03/24'), ]); - $payment_method->setBillingProfile($profile); + $payment_method->setBillingProfile($this->adminProfile); $payment_method->save(); $this->orderPaymentMethod = $this->createEntity('commerce_payment_method', [ @@ -236,7 +243,8 @@ class PaymentCheckoutTest extends CommerceBrowserTestBase { $this->drupalGet('checkout/1'); $radio_button = $page->findField('Example'); $this->assertNull($radio_button); - $this->assertSession()->fieldExists('payment_information[billing_information][address][0][address][postal_code]'); + $this->createScreenshot(); + $this->assertEquals($page->findField('Select an address')->getValue(), $this->adminProfile->id()); } /** @@ -273,6 +281,8 @@ class PaymentCheckoutTest extends CommerceBrowserTestBase { * Tests checkout with a new payment method. */ public function testCheckoutWithNewPaymentMethod() { + $profile = $this->createUserProfile(); + // Test the 'capture' setting of PaymentProcess while here. /** @var \Drupal\commerce_checkout\Entity\CheckoutFlow $checkout_flow */ $checkout_flow = CheckoutFlow::load('default'); @@ -289,17 +299,12 @@ class PaymentCheckoutTest extends CommerceBrowserTestBase { $radio_button->click(); $this->waitForAjaxToFinish(); + $this->getSession()->getPage()->fillField('Select an address', $profile->id()); $this->submitForm([ 'payment_information[add_payment_method][payment_details][number]' => '4012888888881881', 'payment_information[add_payment_method][payment_details][expiration][month]' => '02', 'payment_information[add_payment_method][payment_details][expiration][year]' => '2020', 'payment_information[add_payment_method][payment_details][security_code]' => '123', - 'payment_information[add_payment_method][billing_information][address][0][address][given_name]' => 'Johnny', - 'payment_information[add_payment_method][billing_information][address][0][address][family_name]' => 'Appleseed', - 'payment_information[add_payment_method][billing_information][address][0][address][address_line1]' => '123 New York Drive', - 'payment_information[add_payment_method][billing_information][address][0][address][locality]' => 'New York City', - 'payment_information[add_payment_method][billing_information][address][0][address][administrative_area]' => 'NY', - 'payment_information[add_payment_method][billing_information][address][0][address][postal_code]' => '10001', ], 'Continue to review'); $this->assertSession()->pageTextContains('Payment information'); $this->assertSession()->pageTextContains('Visa ending in 1881'); @@ -327,6 +332,8 @@ class PaymentCheckoutTest extends CommerceBrowserTestBase { * Tests that a declined payment does not complete checkout. */ public function testCheckoutWithDeclinedPaymentMethod() { + $profile = $this->createUserProfile(); + $this->drupalGet($this->product->toUrl()->toString()); $this->submitForm([], 'Add to cart'); $this->drupalGet('checkout/1'); @@ -334,17 +341,14 @@ class PaymentCheckoutTest extends CommerceBrowserTestBase { $radio_button->click(); $this->waitForAjaxToFinish(); + $this->getSession()->getPage()->fillField('Select an address', $profile->id()); + $this->waitForAjaxToFinish(); + $this->createScreenshot(); $this->submitForm([ 'payment_information[add_payment_method][payment_details][number]' => '4111111111111111', 'payment_information[add_payment_method][payment_details][expiration][month]' => '02', 'payment_information[add_payment_method][payment_details][expiration][year]' => '2020', 'payment_information[add_payment_method][payment_details][security_code]' => '123', - 'payment_information[add_payment_method][billing_information][address][0][address][given_name]' => 'Johnny', - 'payment_information[add_payment_method][billing_information][address][0][address][family_name]' => 'Appleseed', - 'payment_information[add_payment_method][billing_information][address][0][address][address_line1]' => '123 New York Drive', - 'payment_information[add_payment_method][billing_information][address][0][address][locality]' => 'Somewhere', - 'payment_information[add_payment_method][billing_information][address][0][address][administrative_area]' => 'WI', - 'payment_information[add_payment_method][billing_information][address][0][address][postal_code]' => '53140', ], 'Continue to review'); $this->assertSession()->pageTextContains('Payment information'); $this->assertSession()->pageTextContains('Visa ending in 1111'); @@ -365,6 +369,8 @@ class PaymentCheckoutTest extends CommerceBrowserTestBase { * Tests checkout with an off-site gateway (POST redirect method). */ public function testCheckoutWithOffsiteRedirectPost() { + $profile = $this->createUserProfile(); + $this->drupalGet($this->product->toUrl()->toString()); $this->submitForm([], 'Add to cart'); $this->drupalGet('checkout/1'); @@ -372,14 +378,9 @@ class PaymentCheckoutTest extends CommerceBrowserTestBase { $radio_button->click(); $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'); + $this->getSession()->getPage()->fillField('Select an address', $profile->id()); + $this->waitForAjaxToFinish(); + $this->submitForm([], 'Continue to review'); $this->assertSession()->pageTextContains('Payment information'); $this->assertSession()->pageTextContains('Example'); $this->assertSession()->pageTextContains('Johnny Appleseed'); @@ -403,6 +404,8 @@ class PaymentCheckoutTest extends CommerceBrowserTestBase { * page in order to proceed to the gateway. */ public function testCheckoutWithOffsiteRedirectPostManual() { + $profile = $this->createUserProfile(); + $payment_gateway = PaymentGateway::load('offsite'); $payment_gateway->getPlugin()->setConfiguration([ 'redirect_method' => 'post_manual', @@ -417,14 +420,9 @@ class PaymentCheckoutTest extends CommerceBrowserTestBase { $radio_button->click(); $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'); + $this->getSession()->getPage()->fillField('Select an address', $profile->id()); + $this->waitForAjaxToFinish(); + $this->submitForm([], 'Continue to review'); $this->assertSession()->pageTextContains('Payment information'); $this->assertSession()->pageTextContains('Example'); $this->assertSession()->pageTextContains('Johnny Appleseed'); @@ -453,6 +451,8 @@ class PaymentCheckoutTest extends CommerceBrowserTestBase { * Tests checkout with an off-site gateway (GET redirect method). */ public function testCheckoutWithOffsiteRedirectGet() { + $profile = $this->createUserProfile(); + // Checkout must work when the off-site gateway is alone, and the // radio button hidden. $onsite_gateway = PaymentGateway::load('onsite'); @@ -473,14 +473,9 @@ class PaymentCheckoutTest extends CommerceBrowserTestBase { $this->submitForm([], 'Add to cart'); $this->drupalGet('checkout/1'); - $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'); + $this->getSession()->getPage()->fillField('Select an address', $profile->id()); + $this->waitForAjaxToFinish(); + $this->submitForm([], 'Continue to review'); $this->assertSession()->pageTextContains('Payment information'); $this->assertSession()->pageTextContains('Example'); $this->assertSession()->pageTextContains('Johnny Appleseed'); @@ -503,6 +498,8 @@ class PaymentCheckoutTest extends CommerceBrowserTestBase { * The off-site form throws an exception, simulating an API fail. */ public function testFailedCheckoutWithOffsiteRedirectGet() { + $profile = $this->createUserProfile(); + $payment_gateway = PaymentGateway::load('offsite'); $payment_gateway->getPlugin()->setConfiguration([ 'redirect_method' => 'get', @@ -517,14 +514,9 @@ class PaymentCheckoutTest extends CommerceBrowserTestBase { $radio_button->click(); $this->waitForAjaxToFinish(); - $this->submitForm([ - 'payment_information[billing_information][address][0][address][given_name]' => 'Johnny', - 'payment_information[billing_information][address][0][address][family_name]' => 'FAIL', - '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'); + $this->getSession()->getPage()->fillField('Select an address', $profile->id()); + $this->waitForAjaxToFinish(); + $this->submitForm([], 'Continue to review'); $this->assertSession()->pageTextContains('Payment information'); $this->assertSession()->pageTextContains('Example'); $this->assertSession()->pageTextContains('Johnny FAIL'); @@ -546,6 +538,8 @@ class PaymentCheckoutTest extends CommerceBrowserTestBase { * Tests checkout with a manual gateway. */ public function testCheckoutWithManual() { + $profile = $this->createUserProfile(); + $this->drupalGet($this->product->toUrl()->toString()); $this->submitForm([], 'Add to cart'); $this->drupalGet('checkout/1'); @@ -553,14 +547,9 @@ class PaymentCheckoutTest extends CommerceBrowserTestBase { $radio_button->click(); $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'); + $this->getSession()->getPage()->fillField('Select an address', $profile->id()); + $this->waitForAjaxToFinish(); + $this->submitForm([], 'Continue to review'); $this->assertSession()->pageTextContains('Payment information'); $this->assertSession()->pageTextContains('Example'); $this->assertSession()->pageTextContains('Johnny Appleseed'); @@ -582,6 +571,7 @@ class PaymentCheckoutTest extends CommerceBrowserTestBase { * Tests a free order, where only the billing information is collected. */ public function testFreeOrder() { + $profile = $this->createUserProfile(); $this->drupalGet($this->product->toUrl()->toString()); $this->submitForm([], 'Add to cart'); @@ -595,17 +585,12 @@ class PaymentCheckoutTest extends CommerceBrowserTestBase { ])); $order->save(); + $this->getSession()->getPage()->fillField('Select an address', $profile->id()); + $this->waitForAjaxToFinish(); $this->drupalGet('checkout/1'); $this->assertSession()->pageTextContains('Billing information'); $this->assertSession()->pageTextNotContains('Payment information'); - $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'); + $this->submitForm([], 'Continue to review'); $this->assertSession()->pageTextContains('Billing information'); $this->assertSession()->pageTextNotContains('Payment information'); @@ -617,4 +602,26 @@ class PaymentCheckoutTest extends CommerceBrowserTestBase { $this->assertSession()->pageTextContains('Your order number is 1. You can view your order on your account page when logged in.'); } + /** + * Create user profile. + * + * @return \Drupal\profile\Entity\ProfileInterface + */ + function createUserProfile() { + $profile = $this->createEntity('profile', [ + 'type' => 'customer', + 'address' => [ + 'country_code' => 'US', + 'postal_code' => '10001', + 'locality' => 'New York City', + 'address_line1' => '123 New York Drive', + 'administrative_area' => 'NY', + 'given_name' => 'Johnny', + 'family_name' => 'Appleseed', + ], + 'uid' => $this->adminUser->id(), + ]); + return $profile; + } + } diff --git a/modules/promotion/tests/src/FunctionalJavascript/CouponRedemptionPaneTest.php b/modules/promotion/tests/src/FunctionalJavascript/CouponRedemptionPaneTest.php index 8cbf9a3..7376133 100644 --- a/modules/promotion/tests/src/FunctionalJavascript/CouponRedemptionPaneTest.php +++ b/modules/promotion/tests/src/FunctionalJavascript/CouponRedemptionPaneTest.php @@ -34,6 +34,13 @@ class CouponRedemptionPaneTest extends CommerceBrowserTestBase { protected $cartManager; /** + * The profile. + * + * @var \Drupal\profile\Entity\ProfileInterface + */ + protected $profile; + + /** * The promotion. * * @var \Drupal\commerce_promotion\Entity\PromotionInterface @@ -125,7 +132,7 @@ class CouponRedemptionPaneTest extends CommerceBrowserTestBase { ]); $onsite_gateway->save(); - $profile = $this->createEntity('profile', [ + $this->profile = $this->createEntity('profile', [ 'type' => 'customer', 'address' => [ 'country_code' => 'US', @@ -283,24 +290,31 @@ class CouponRedemptionPaneTest extends CommerceBrowserTestBase { * Tests that adding/removing coupons does not submit other panes. */ public function testCheckoutSubmit() { + $page = $this->getSession()->getPage(); // 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'); + + $this->assertEquals($page->findField('Select a profile')->getValue(), $this->profile->id()); + $this->submitForm([], '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'); + $profile = $this->createEntity('profile', [ + 'type' => 'customer', + 'address' => [ + 'country_code' => 'US', + 'postal_code' => '10001', + 'locality' => 'New York City', + 'address_line1' => '123 New York Drive', + 'administrative_area' => 'NY', + 'given_name' => 'Johnny', + 'family_name' => 'Appleseed', + ], + 'uid' => $this->adminUser->id(), + ]); + $this->getSession()->getPage()->fillField('Select a profile', $profile->id()); // Add a coupon. $coupons = $this->promotion->getCoupons();