0

I want to test a user editing a form, and in that form there is a <select multiple="multiple">. How do I select or unselect <option>values from that form widget in the functional test?

Form setup:

'regions_list' => new sfWidgetFormDoctrineChoice(array(
  'multiple' => true, 
  'model' => 'Region'
)),

Functional Test:

$browser->
  getRoute('profile')->

  //setField('profile[regions_list]', '[9][8]')->  // tried syntaxmany combinations
  click('Save Profile', array(
    'profile' => array(
      // 'regions_list' => ???,
      // I've tried many combinations even with setField and none made sense and where testable so far
    )
  ))->

  with('form')->begin()->
    hasErrors(0)->
    hasGlobalError(0)->
  end()
;
bksunday
  • 715
  • 5
  • 21
  • you tried `setField("profile[regions_list]", array(8,9))`? – prodigitalson Nov 04 '11 at 05:24
  • I did and tried again now and with 1.4.5 it doesn't work =( I'll try to make a test from scratch with latest 1.4.x to be sure not to create confusion if I am the only one not getting it! – bksunday Nov 04 '11 at 12:29

2 Answers2

0

I've never put the with('form') around the click() and this is working on my tests:

$browser->
  getRoute('profile')->

  click('Save Profile', array(
    'profile' => array('regions_list' => array(8,9)
        // with: <option value="8">something</option><option value="9">else</option>
        // ... and other fields
    )
  ))->
  end()->

  with('form')->begin()->
    debug() -> // added this line so you can see what is happening
    hasErrors(0)->
    hasGlobalError(0)->
  end()
;
samura
  • 4,375
  • 1
  • 19
  • 26
  • While posting my question yesterday I did thought about that ;) This project I am using is on 1.4.5 and it doesn't work =( but I guess I will try with the latest version maybe? – bksunday Nov 04 '11 at 12:27
  • To be more precise, I get a regions_list Invalid and the posted regions_list values from debug()-> shows an array with the two values above (8, 9) followed by the values already there so those also don't get unselected. – bksunday Nov 04 '11 at 12:34
  • I get the `[Invalid.]` when I'm trying to add a `regions` id that does not exist. – samura Nov 04 '11 at 13:54
  • wouldn't that be just the validator doing its job? – bksunday Nov 04 '11 at 16:31
  • yes. exactly. So try to see if [8,9] are id's that do exist in your `regions` table. – samura Nov 04 '11 at 17:39
0

Well, after digging in the click() process, I can't get the result I expect.

The solutions follows the problem:

in a <select multiple> tag, I expect that if I don't post any values, it's going to repost it as is, and if I do specify values, it will post only those and not try to control on/off by values.

The way it's reacting now is that the initial values and the chosen values are merged together and the initial values keys matching the chosen values keys are replaced which doesn't make sense to me because those keys or their order are irrelevant. I only care about the option values chosen.

The answer to my question is: You can't do what you expect with the current implementation.

The solution: Override (or submit a patch if you have time) the doClickElement function in sfBrowserBase class which could look like this: ( look for the 3 comments // fix for select multiple and the last 2 methods to add )

public function doClickElement(DOMElement $item, $arguments = array(), $options = array())
{
  $method = strtolower(isset($options['method']) ? $options['method'] : 'get');

  if ('a' == $item->nodeName)
  {
    if (in_array($method, array('post', 'put', 'delete')))
    {
      if (isset($options['_with_csrf']) && $options['_with_csrf'])
      {
        $arguments['_with_csrf'] = true;
      }

      return array($item->getAttribute('href'), $method, $arguments);
    }
    else
    {
      return array($item->getAttribute('href'), 'get', $arguments);
    }
  }
  else if ('button' == $item->nodeName || ('input' == $item->nodeName && in_array($item->getAttribute('type'), array('submit', 'button', 'image'))))
  {
    // add the item's value to the arguments
    $this->parseArgumentAsArray($item->getAttribute('name'), $item->getAttribute('value'), $arguments);

    // use the ancestor form element
    do
    {
      if (null === $item = $item->parentNode)
      {
        throw new Exception('The clicked form element does not have a form ancestor.');
      }
    }
    while ('form' != $item->nodeName);
  }

  // form attributes
  $url = $item->getAttribute('action');
  if (!$url || '#' == $url)
  {
    $url = $this->stack[$this->stackPosition]['uri'];
  }
  $method = strtolower(isset($options['method']) ? $options['method'] : ($item->getAttribute('method') ? $item->getAttribute('method') : 'get'));

  // merge form default values and arguments
  $defaults = array();
  $arguments = sfToolkit::arrayDeepMerge($this->fields, $arguments);

  // fix for select multiple
  $select_multiple_to_check = array();

  $xpath = $this->getResponseDomXpath();
  foreach ($xpath->query('descendant::input | descendant::textarea | descendant::select', $item) as $element)
  {
    if ($element->hasAttribute('disabled'))
    {
      continue;
    }

    $elementName = $element->getAttribute('name');
    $nodeName    = $element->nodeName;
    $value       = null;

    if ($nodeName == 'input' && ($element->getAttribute('type') == 'checkbox' || $element->getAttribute('type') == 'radio'))
    {
      // fix for select multiple
      if (substr($elementName, -2) == '[]') 
      {
        $select_multiple_to_check[$elementName] = true;
      }

      if ($element->getAttribute('checked'))
      {
        $value = $element->hasAttribute('value') ? $element->getAttribute('value') : '1';
      }
    }
    else if ($nodeName == 'input' && $element->getAttribute('type') == 'file')
    {
      $filename = array_key_exists($elementName, $arguments) ? $arguments[$elementName] : sfToolkit::getArrayValueForPath($arguments, $elementName, '');

      if (is_readable($filename))
      {
        $fileError = UPLOAD_ERR_OK;
        $fileSize = filesize($filename);
      }
      else
      {
        $fileError = UPLOAD_ERR_NO_FILE;
        $fileSize = 0;
      }

      unset($arguments[$elementName]);

      $this->parseArgumentAsArray($elementName, array('name' => basename($filename), 'type' => '', 'tmp_name' => $filename, 'error' => $fileError, 'size' => $fileSize), $this->files);
    }
    else if ('input' == $nodeName && !in_array($element->getAttribute('type'), array('submit', 'button', 'image')))
    {
      $value = $element->getAttribute('value');
    }
    else if ($nodeName == 'textarea')
    {
      $value = '';
      foreach ($element->childNodes as $el)
      {
        $value .= $this->getResponseDom()->saveXML($el);
      }
    }
    else if ($nodeName == 'select')
    {
      if ($multiple = $element->hasAttribute('multiple'))
      {
        // fix for select multiple
        $select_multiple_to_check[$elementName] = true;

        $elementName = str_replace('[]', '', $elementName);
        $value = array();
      }
      else
      {
        $value = null;
      }

      $found = false;
      foreach ($xpath->query('descendant::option', $element) as $option)
      {
        if ($option->getAttribute('selected'))
        {
          $found = true;
          if ($multiple)
          {
            $value[] = $option->getAttribute('value');
          }
          else
          {
            $value = $option->getAttribute('value');
          }
        }
      }
      $option = $xpath->query('descendant::option', $element)->item(0);
      if (!$found && !$multiple && $option instanceof DOMElement)
      {
        $value = $option->getAttribute('value');
      }


    }

    if (null !== $value)
    {
      $this->parseArgumentAsArray($elementName, $value, $defaults);
    }
  }

  // fix for select multiple
  foreach($select_multiple_to_check as $elementName => $uselessbool)
  {
    $path = array_filter(preg_split('/(\[ | \[\] | \])/x', $elementName), create_function('$s', 'return $s !== "";'));
    if ($this->findInArrayByArrayPath($arguments, $path) !== false)
    {
      $this->unsetInArrayByArrayPath($defaults, $path);
    }
  }

  $arguments = sfToolkit::arrayDeepMerge($defaults, $arguments);

  if (in_array($method, array('post', 'put', 'delete')))
  {
    return array($url, $method, $arguments);
  }
  else
  {
    $queryString = http_build_query($arguments, null, '&');
    $sep = false === strpos($url, '?') ? '?' : '&';

    return array($url.($queryString ? $sep.$queryString : ''), 'get', array());
  }
}

// fix for select multiple
// taken from http://stackoverflow.com/questions/3145068/set-multi-dimensional-array-by-key-path-from-array-values/3145199#3145199
public function findInArrayByArrayPath(&$array, &$path, $_i=0) {
  // sanity check
  if ( !(is_array($array) && is_array($path)) ) return false;
  $c = count($path); if ($_i >= $c) return false;

  if ($_i==0) {$path = array_values($path);} // to make sure we don't get skipped numeric keys which does happens in the preg_split above

  $k = $path[$_i];
  if (array_key_exists($k, $array))
    return ($_i == $c-1) ? $array[$k] : $this->findInArrayByArrayPath($array[$k], $path, $_i+1);
  else
    return false;
}

// fix for select multiple
public function unsetInArrayByArrayPath(&$array, &$path, $_i=0) {
  // sanity check
  if ( !(is_array($array) && is_array($path)) ) return false;
  $c = count($path); if ($_i >= $c) return false;

  if ($_i==0) {$path = array_values($path);} // to make sure we don't get skipped numeric keys which does happens in the preg_split above

  $k = $path[$_i];
  if (array_key_exists($k, $array))
    if ($_i == $c-1) {
      unset($array[$k]);
      return true;
    } else {
      return $this->unsetInArrayByArrayPath($array[$k], $path, $_i+1);
    }
  else
    return false;
}

And Now: 'profile' => array('regions_list' => array(8,9)) works for me.

bksunday
  • 715
  • 5
  • 21