Index: includes/form.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/form.inc,v
retrieving revision 1.372
diff -u -r1.372 form.inc
--- includes/form.inc	11 Sep 2009 04:09:26 -0000	1.372
+++ includes/form.inc	12 Sep 2009 20:27:30 -0000
@@ -787,7 +787,9 @@
       // A simple call to empty() will not cut it here as some fields, like
       // checkboxes, can return a valid value of '0'. Instead, check the
       // length if it's a string, and the item count if it's an array.
-      if ($elements['#required'] && (!count($elements['#value']) || (is_string($elements['#value']) && strlen(trim($elements['#value'])) == 0))) {
+      // Unchecked checkbox has #value of numeric 0 (different than 
+	  // string '0', which could be a valid value).
+      if ($elements['#required'] && (!count($elements['#value']) || (is_string($elements['#value']) && strlen(trim($elements['#value'])) == 0) || $elements['#value'] === 0)) {
         form_error($elements, $t('!name field is required.', array('!name' => $elements['#title'])));
       }
 
@@ -1315,9 +1317,15 @@
 function form_type_checkbox_value($element, $input = FALSE) {
   if ($input !== FALSE) {
     if (empty($element['#disabled'])) {
-      return !empty($input) ? $element['#return_value'] : 0;
+      // Successful (checked) checkboxes are present with a value (possibly '0').
+      // http://www.w3.org/TR/html401/interact/forms.html#successful-controls
+      // For an unchecked checkbox, we return numeric 0 (something we can
+      // explicitly test for; different than string '0').
+      return isset($input) ? $element['#return_value'] : 0;
     }
     else {
+      // Disabled form controls are not submitted by the browser.
+      // Ignore any submitted value and always return default.
       return $element['#default_value'];
     }
   }
@@ -1997,17 +2005,20 @@
  * @ingroup themeable
  */
 function theme_checkbox($element) {
+  $t = get_t();
   _form_set_class($element, array('form-checkbox'));
   $checkbox = '<input ';
   $checkbox .= 'type="checkbox" ';
   $checkbox .= 'name="' . $element['#name'] . '" ';
   $checkbox .= 'id="' . $element['#id'] . '" ' ;
   $checkbox .= 'value="' . $element['#return_value'] . '" ';
-  $checkbox .= $element['#value'] ? ' checked="checked" ' : ' ';
+  // Unchecked checkbox has #value of numeric 0.
+  $checkbox .= ($element['#value'] !== 0 && $element['#value'] == $element['#return_value']) ? ' checked="checked" ' : ' ';
   $checkbox .= drupal_attributes($element['#attributes']) . ' />';
 
   if (!is_null($element['#title'])) {
-    $checkbox = '<label class="option" for="' . $element['#id'] . '">' . $checkbox . ' ' . $element['#title'] . '</label>';
+    $required = !empty($element['#required']) ? ' <span class="form-required" title="' . $t('This field is required.') . '">*</span>' : '';
+    $checkbox = '<label class="option" for="' . $element['#id'] . '">' . $checkbox . ' ' . $element['#title'] . $required . '</label>';
   }
 
   return $checkbox;
@@ -2061,7 +2072,7 @@
           '#processed' => TRUE,
           '#title' => $choice,
           '#return_value' => $key,
-          '#default_value' => isset($value[$key]),
+          '#default_value' => isset($value[$key]) ? $key : NULL,
           '#attributes' => $element['#attributes'],
           '#ajax' => isset($element['#ajax']) ? $element['#ajax'] : NULL,
         );
@@ -2174,7 +2185,7 @@
             '#type' => 'checkbox',
             '#title' => '',
             '#return_value' => $key,
-            '#default_value' => isset($value[$key]),
+            '#default_value' => isset($value[$key]) ? $key : NULL,
             '#attributes' => $element['#attributes'],
             '#ajax' => isset($element['#ajax']) ? $element['#ajax'] : NULL,
           );
Index: modules/simpletest/tests/form.test
===================================================================
RCS file: /cvs/drupal/drupal/modules/simpletest/tests/form.test,v
retrieving revision 1.14
diff -u -r1.14 form.test
--- modules/simpletest/tests/form.test	13 Jul 2009 21:51:41 -0000	1.14
+++ modules/simpletest/tests/form.test	5 Sep 2009 06:27:14 -0000
@@ -6,26 +6,34 @@
  * Unit tests for the Drupal Form API.
  */
 
-class FormsTestCase extends DrupalWebTestCase {
+class FormsAPIUnitTest extends DrupalWebTestCase {
 
   public static function getInfo() {
     return array(
-      'name' => 'Required field validation',
-      'description' => 'Carriage returns, tabs, and spaces are not valid content for a required field.',
+      'name' => 'Field validation',
+      'description' => 'Test various field validation mechanisms.',
       'group' => 'Form API',
     );
   }
 
+  function setUp() {
+    parent::setUp('form_test');
+  }
+
   /**
    * Check several empty values for required forms elements.
    *
+   * Carriage returns, tabs, spaces, and unchecked checkbox elements are not
+   * valid content for a required field.
+   *
    * If the form field is found in form_get_errors() then the test pass.
    */
   function testRequiredFields() {
     // Originates from http://drupal.org/node/117748
-    // Sets of empty strings and arrays
+    // Sets of empty strings and arrays.
     $empty_strings = array('""' => "", '"\n"' => "\n", '" "' => " ", '"\t"' => "\t", '" \n\t "' => " \n\t ", '"\n\n\n\n\n"' => "\n\n\n\n\n");
     $empty_arrays = array('array()' => array());
+    $empty_checkbox = array(NULL);
 
     $elements['textfield']['element'] = array('#title' => $this->randomName(), '#type' => 'textfield', '#required' => TRUE);
     $elements['textfield']['empty_values'] = $empty_strings;
@@ -42,6 +50,9 @@
     $elements['radios']['element'] = array('#title' => $this->randomName(), '#type' => 'radios', '#required' => TRUE, '#options' => array($this->randomName(), $this->randomName(), $this->randomName()));
     $elements['radios']['empty_values'] = $empty_arrays;
 
+    $elements['checkbox']['element'] = array('#title' => $this->randomName(), '#type' => 'checkbox', '#required' => TRUE, '#title' => $this->randomName());
+    $elements['checkbox']['empty_values'] = $empty_checkbox;
+
     $elements['checkboxes']['element'] = array('#title' => $this->randomName(), '#type' => 'checkboxes', '#required' => TRUE, '#options' => array($this->randomName(), $this->randomName(), $this->randomName()));
     $elements['checkboxes']['empty_values'] = $empty_arrays;
 
@@ -51,7 +62,7 @@
     $elements['file']['element'] = array('#title' => $this->randomName(), '#type' => 'file', '#required' => TRUE);
     $elements['file']['empty_values'] = $empty_strings;
 
-    // Go through all the elements and all the empty values for them
+    // Go through all the elements and all the empty values for them.
     foreach ($elements as $type => $data) {
       foreach ($data['empty_values'] as $key => $empty) {
         $form_id = $this->randomName();
@@ -72,42 +83,33 @@
     // Clear the expected form error messages so they don't appear as exceptions.
     drupal_get_messages();
   }
-}
-
-/**
- * Test form type functions for expected behavior.
- */
-class FormsTestTypeCase extends DrupalUnitTestCase {
-  public static function getInfo() {
-    return array(
-      'name' => 'Form type-specific tests',
-      'description' => 'Test form type functions for expected behavior.',
-      'group' => 'Form API',
-    );
-  }
 
   /**
-   * Test form_type_checkbox_value() function for expected behavior.
+   * Test default value handling for checkboxes.
+   *
+   * @see form_test_checkbox().
    */
-  function testFormCheckboxValue() {
-    $form['#return_value'] = $return_value = $this->randomName();
-    $form['#default_value'] = $default_value = $this->randomName();
-    // Element is disabled , and $edit is not empty.
-    $form['#disabled'] = TRUE;
-    $edit = array(1);
-    $this->assertEqual(form_type_checkbox_value($form, $edit), $default_value, t('form_type_checkbox_value() returns the default value when #disabled is set.'));
-
-    // Element is not disabled, $edit is not empty.
-    unset($form['#disabled']);
-    $this->assertEqual(form_type_checkbox_value($form, $edit), $return_value, t('form_type_checkbox_value() returns the return value when #disabled is not set.'));
+  function testCheckBoxProcessing() {
+    // First, try to submit without the required checkbox.
+    $this->drupalPost('form-test/checkbox', array(), t('Submit'));
+    if ($this->assertRaw(t('!name field is required.', array('!name' => 'required_checkbox')), t('A required checkbox is actually mandatory'))) {
+      // Now try to submit the form correctly.
+      $this->drupalPost(NULL, array('required_checkbox' => 1), t('Submit'));
+    }
 
-    // Element is not disabled, $edit is empty.
-    $edit = array();
-    $this->assertIdentical(form_type_checkbox_value($form, $edit), 0, t('form_type_checkbox_value() returns 0 when #disabled is not set, and $edit is empty.'));
+    $values = json_decode($this->drupalGetContent(), TRUE);
+    $expected_values = array(
+      'disabled_checkbox_on' => 'disabled_checkbox_on',
+      'disabled_checkbox_off' => '',
+      'checkbox_on' => 'checkbox_on',
+      'checkbox_off' => '',
+      'zero_checkbox_on' => '0',
+      'zero_checkbox_off' => '',
+    );
 
-    // $edit is FALSE.
-    $edit = FALSE;
-    $this->assertNull(form_type_checkbox_value($form, $edit), t('form_type_checkbox_value() returns NULL when $edit is FALSE'));
+    foreach ($expected_values as $widget => $expected_value) {
+      $this->assertEqual($values[$widget], $expected_value, t('Checkbox %widget returns expected value (expected: %expected, got: %value)', array('%widget' => var_export($widget, TRUE), '%expected' => var_export($expected_value, TRUE), '%value' => var_export($values[$widget], TRUE))));
+    }
   }
 }
 
Index: modules/simpletest/tests/form_test.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/simpletest/tests/form_test.module,v
retrieving revision 1.8
diff -u -r1.8 form_test.module
--- modules/simpletest/tests/form_test.module	17 Aug 2009 07:12:16 -0000	1.8
+++ modules/simpletest/tests/form_test.module	5 Sep 2009 06:27:14 -0000
@@ -66,6 +66,14 @@
     'type' => MENU_CALLBACK,
   );
 
+  $items['form-test/checkbox'] = array(
+    'title' => t('Form test'),
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('_form_test_checkbox'),
+    'access callback' => TRUE,
+    'type' => MENU_CALLBACK,
+  );
+
   return $items;
 }
 
@@ -365,3 +373,80 @@
   $form_state['storage']['step']++;
   drupal_set_message("Form constructions: ". $_SESSION['constructions']);
 }
+
+/**
+ * Build a form to test a checkbox.
+ */
+function _form_test_checkbox() {
+  $form = array();
+
+  // A required checkbox.
+  $form['required_checkbox'] = array(
+    '#type' => 'checkbox',
+    '#required' => TRUE,
+    '#title' => 'required_checkbox',
+  );
+
+  // A disabled checkbox should get its default value back.
+  $form['disabled_checkbox_on'] = array(
+    '#type' => 'checkbox',
+    '#disabled' => TRUE,
+    '#return_value' => 'disabled_checkbox_on',
+    '#default_value' => 'disabled_checkbox_on',
+    '#title' => 'disabled_checkbox_on',
+  );
+  $form['disabled_checkbox_off'] = array(
+    '#type' => 'checkbox',
+    '#disabled' => TRUE,
+    '#return_value' => 'disabled_checkbox_off',
+    '#default_value' => NULL,
+    '#title' => 'disabled_checkbox_off',
+  );
+
+  // A checkbox is active when #default_value == #return_value.
+  $form['checkbox_on'] = array(
+    '#type' => 'checkbox',
+    '#return_value' => 'checkbox_on',
+    '#default_value' => 'checkbox_on',
+    '#title' => 'checkbox_on',
+  );
+
+  // But inactive in any other case.
+  $form['checkbox_off'] = array(
+    '#type' => 'checkbox',
+    '#return_value' => 'checkbox_off',
+    '#default_value' => 'checkbox_on',
+    '#title' => 'checkbox_off',
+  );
+
+  // Checkboxes with a #return_value of '0' are supported.
+  $form['zero_checkbox_on'] = array(
+    '#type' => 'checkbox',
+    '#return_value' => '0',
+    '#default_value' => '0',
+    '#title' => 'zero_checkbox_on',
+  );
+
+  // In that case, passing a #default_value != '0' means that the checkbox is off.
+  $form['zero_checkbox_off'] = array(
+    '#type' => 'checkbox',
+    '#return_value' => '0',
+    '#default_value' => '1',
+    '#title' => 'zero_checkbox_off',
+  );
+
+  $form['op'] = array(
+    '#type' => 'submit',
+    '#value' => t('Submit')
+  );
+
+  return $form;
+}
+
+/**
+ * Return the form values by JSON.
+ */
+function _form_test_checkbox_submit($form_id, &$form_state) {
+  drupal_json($form_state['values']);
+  exit;
+}

