Index: modules/simpletest/simpletest.info =================================================================== RCS file: /cvs/drupal/drupal/modules/simpletest/simpletest.info,v retrieving revision 1.11 diff -u -r1.11 simpletest.info --- modules/simpletest/simpletest.info 11 Sep 2009 02:19:02 -0000 1.11 +++ modules/simpletest/simpletest.info 11 Sep 2009 20:17:25 -0000 @@ -4,9 +4,12 @@ package = Core version = VERSION core = 7.x +files[] = simpletest.inc +files[] = simpletest.install +files[] = simpletest.js.inc files[] = simpletest.module files[] = simpletest.pages.inc -files[] = simpletest.install +files[] = simpletest.php.inc files[] = simpletest.test files[] = drupal_web_test_case.php Index: modules/simpletest/simpletest.module =================================================================== RCS file: /cvs/drupal/drupal/modules/simpletest/simpletest.module,v retrieving revision 1.71 diff -u -r1.71 simpletest.module --- modules/simpletest/simpletest.module 24 Aug 2009 00:14:21 -0000 1.71 +++ modules/simpletest/simpletest.module 11 Sep 2009 20:17:25 -0000 @@ -6,6 +6,10 @@ * Provides testing functionality. */ +module_load_include('inc', 'simpletest'); +module_load_include('js.inc', 'simpletest'); +module_load_include('php.inc', 'simpletest'); + /** * Implement hook_help(). */ @@ -28,14 +32,24 @@ $items['admin/config/development/testing'] = array( 'title' => 'Testing', 'page callback' => 'drupal_get_form', - 'page arguments' => array('simpletest_test_form'), + 'page arguments' => array('simpletest_php_form'), 'description' => 'Run tests against Drupal core and your active modules. These tests help assure that your site code is working as designed.', 'access arguments' => array('administer unit tests'), - 'file' => 'simpletest.pages.inc', + 'file' => 'simpletest.php.inc', ); - $items['admin/config/development/testing/list'] = array( - 'title' => 'List', + $items['admin/config/development/testing/php'] = array( + 'title' => 'PHP', 'type' => MENU_DEFAULT_LOCAL_TASK, + 'weight' => -10, + ); + $items['admin/config/development/testing/js'] = array( + 'title' => 'JavaScript', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('simpletest_js_form'), + 'access arguments' => array('administer unit tests'), + 'type' => MENU_LOCAL_TASK, + 'file' => 'simpletest.js.inc', + 'weight' => -9, ); $items['admin/config/development/testing/settings'] = array( 'title' => 'Settings', @@ -44,15 +58,45 @@ 'access arguments' => array('administer unit tests'), 'type' => MENU_LOCAL_TASK, 'file' => 'simpletest.pages.inc', + 'weight' => -8, ); - $items['admin/config/development/testing/results/%'] = array( + $items['admin/config/development/testing/results/%/%'] = array( 'title' => 'Test result', 'page callback' => 'drupal_get_form', - 'page arguments' => array('simpletest_result_form', 5), + 'page arguments' => array('simpletest_result_form', 5, 6), 'description' => 'View result of tests.', 'access arguments' => array('administer unit tests'), 'type' => MENU_CALLBACK, - 'file' => 'simpletest.pages.inc', + 'file' => 'simpletest.inc', + ); + + $items['simpletest/js/collect'] = array( + 'title' => 'Testing', + 'page callback' => 'simpletest_js_collect', + 'access arguments' => array('administer unit tests'), + 'type' => MENU_CALLBACK, + 'file' => 'simpletest.js.inc', + ); + $items['simpletest/js/register'] = array( + 'title' => 'Testing', + 'page callback' => 'simpletest_js_register', + 'access arguments' => array('administer unit tests'), + 'type' => MENU_CALLBACK, + 'file' => 'simpletest.js.inc', + ); + $items['simpletest/js/record'] = array( + 'title' => 'Testing', + 'page callback' => 'simpletest_js_record', + 'access arguments' => array('administer unit tests'), + 'type' => MENU_CALLBACK, + 'file' => 'simpletest.js.inc', + ); + $items['simpletest/js/qunit'] = array( + 'title' => 'Testing', + 'page callback' => 'simpletest_js_qunit', + 'access arguments' => array('administer unit tests'), + 'type' => MENU_CALLBACK, + 'file' => 'simpletest.js.inc', ); return $items; } @@ -82,7 +126,28 @@ 'arguments' => array('form' => NULL), 'file' => 'simpletest.pages.inc', ), + 'simpletest_qunit' => array( + 'arguments' => array('scripts' => '', 'styles' => ''), + 'template' => 'simpletest.qunit', + ), + ); +} + +/** + * Implement hook_library(). + */ +function simpletest_library() { + $libraries['qunit'] = array( + 'title' => 'QUnit', + 'website' => 'http://docs.jquery.com/QUnit', + 'version' => 'dev', + 'js' => array( + drupal_get_path('module', 'simpletest') . '/testrunner.js' => array('weight' => -6), + drupal_get_path('module', 'simpletest') . '/test.js' => array('weight' => -5), + ), ); + + return $libraries; } /** @@ -110,138 +175,6 @@ } } -function _simpletest_format_summary_line($summary) { - $args = array( - '@pass' => format_plural(isset($summary['#pass']) ? $summary['#pass'] : 0, '1 pass', '@count passes'), - '@fail' => format_plural(isset($summary['#fail']) ? $summary['#fail'] : 0, '1 fail', '@count fails'), - '@exception' => format_plural(isset($summary['#exception']) ? $summary['#exception'] : 0, '1 exception', '@count exceptions'), - ); - if (!$summary['#debug']) { - return t('@pass, @fail, and @exception', $args); - } - $args['@debug'] = format_plural(isset($summary['#debug']) ? $summary['#debug'] : 0, '1 debug message', '@count debug messages'); - return t('@pass, @fail, @exception, and @debug', $args); -} - -/** - * Actually runs tests. - * - * @param $test_list - * List of tests to run. - * @param $reporter - * Which reporter to use. Allowed values are: text, xml, html and drupal, - * drupal being the default. - */ -function simpletest_run_tests($test_list, $reporter = 'drupal') { - cache_clear_all(); - $test_id = db_insert('simpletest_test_id') - ->useDefaults(array('test_id')) - ->execute(); - - // Clear out the previous verbose files. - file_unmanaged_delete_recursive(file_directory_path() . '/simpletest/verbose'); - - // Get the info for the first test being run. - $first_test = array_shift($test_list); - $first_instance = new $first_test(); - array_unshift($test_list, $first_test); - $info = $first_instance->getInfo(); - - $batch = array( - 'title' => t('Running SimpleTests'), - 'operations' => array( - array('_simpletest_batch_operation', array($test_list, $test_id)), - ), - 'finished' => '_simpletest_batch_finished', - 'progress_message' => '', - 'css' => array(drupal_get_path('module', 'simpletest') . '/simpletest.css'), - 'init_message' => t('Processing test @num of @max - %test.', array('%test' => $info['name'], '@num' => '1', '@max' => count($test_list))), - ); - batch_set($batch); - - module_invoke_all('test_group_started'); - - // Normally, the forms portion of the batch API takes care of calling - // batch_process(), but in the process it saves the whole $form into the - // database (which is huge for the test selection form). - // By calling batch_process() directly, we skip that behavior and ensure - // that we don't exceed the size of data that can be sent to the database - // (max_allowed_packet on MySQL). - batch_process('admin/config/development/testing/results/' . $test_id); -} - -/** - * Batch operation callback. - */ -function _simpletest_batch_operation($test_list_init, $test_id, &$context) { - // Get working values. - if (!isset($context['sandbox']['max'])) { - // First iteration: initialize working values. - $test_list = $test_list_init; - $context['sandbox']['max'] = count($test_list); - $test_results = array('#pass' => 0, '#fail' => 0, '#exception' => 0, '#debug' => 0); - } - else { - // Nth iteration: get the current values where we last stored them. - $test_list = $context['sandbox']['tests']; - $test_results = $context['sandbox']['test_results']; - } - $max = $context['sandbox']['max']; - - // Perform the next test. - $test_class = array_shift($test_list); - $test = new $test_class($test_id); - $test->run(); - $size = count($test_list); - $info = $test->getInfo(); - - module_invoke_all('test_finished', $test->results); - - // Gather results and compose the report. - $test_results[$test_class] = $test->results; - foreach ($test_results[$test_class] as $key => $value) { - $test_results[$key] += $value; - } - $test_results[$test_class]['#name'] = $info['name']; - $items = array(); - foreach (element_children($test_results) as $class) { - array_unshift($items, '
' . t('@name: @summary', array('@name' => $test_results[$class]['#name'], '@summary' => _simpletest_format_summary_line($test_results[$class]))) . '
'); - } - $context['message'] = t('Processed test @num of @max - %test.', array('%test' => $info['name'], '@num' => $max - $size, '@max' => $max)); - $context['message'] .= '
Overall results: ' . _simpletest_format_summary_line($test_results) . '
'; - $context['message'] .= theme('item_list', $items); - - // Save working values for the next iteration. - $context['sandbox']['tests'] = $test_list; - $context['sandbox']['test_results'] = $test_results; - // The test_id is the only thing we need to save for the report page. - $context['results']['test_id'] = $test_id; - - // Multistep processing: report progress. - $context['finished'] = 1 - $size / $max; -} - -function _simpletest_batch_finished($success, $results, $operations, $elapsed) { - if ($success) { - drupal_set_message(t('The test run finished in @elapsed.', array('@elapsed' => $elapsed))); - } - else { - // Use the test_id passed as a parameter to _simpletest_batch_operation(). - $test_id = $operations[0][1][1]; - - // Retrieve the last database prefix used for testing and the last test - // class that was run from. Use the information to read the lgo file - // in case any fatal errors caused the test to crash. - list($last_prefix, $last_test_class) = simpletest_last_test_get($test_id); - simpletest_log_read($test_id, $last_prefix, $last_test_class); - - - drupal_set_message(t('The test run did not successfully finish.'), 'error'); - drupal_set_message(t('Please use the Clean environment button to clean-up temporary files and tables.'), 'warning'); - } - module_invoke_all('test_group_finished'); -} - /* * Get information about the last test that ran given a test ID. * @@ -300,76 +233,6 @@ } /** - * Get a list of all of the tests provided by the system. - * - * The list of test classes is loaded from the registry where it looks for - * files ending in ".test". Once loaded the test list is cached and stored in - * a static variable. In order to list tests provided by disabled modules - * hook_registry_files_alter() is used to forcefully add them to the registry. - * - * @return - * An array of tests keyed with the groups specified in each of the tests - * getInfo() method and then keyed by the test class. An example of the array - * structure is provided below. - * - * @code - * $groups['Blog'] => array( - * 'BlogTestCase' => array( - * 'name' => 'Blog functionality', - * 'description' => 'Create, view, edit, delete, ...', - * 'group' => 'Blog', - * ), - * ); - * @endcode - * @see simpletest_registry_files_alter() - */ -function simpletest_test_get_all() { - $groups = &drupal_static(__FUNCTION__); - - if (!$groups) { - // Load test information from cache if available, otherwise retrieve the - // information from each tests getInfo() method. - if ($cache = cache_get('simpletest', 'cache')) { - $groups = $cache->data; - } - else { - // Select all clases in files ending with .test. - $classes = db_select('registry') - ->fields('registry', array('name')) - ->condition('type', 'class') - ->condition('filename', '%.test', 'LIKE') - ->execute(); - - $groups = array(); - - // Check that each class has a getInfo() method and store the information - // in an array keyed with the group specified in the test information. - foreach ($classes as $class) { - $class = $class->name; - if (class_exists($class) && method_exists($class, 'getInfo')) { - // Valid test class, retrieve test information. - $info = call_user_func(array($class, 'getInfo')); - - // Initialize test groups. - if (!isset($groups[$info['group']])) { - $groups[$info['group']] = array(); - } - $groups[$info['group']][$class] = $info; - } - } - // Sort the groups and tests within the groups by name. - uksort($groups, 'strnatcasecmp'); - foreach ($groups as $group => &$tests) { - uksort($tests, 'strnatcasecmp'); - } - - cache_set('simpletest', $groups); - } - } - return $groups; -} - -/** * Implementation of hook_registry_files_alter(). * * Add the test files for disabled modules so that we get a list containing Index: modules/simpletest/simpletest.pages.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/simpletest/simpletest.pages.inc,v retrieving revision 1.16 diff -u -r1.16 simpletest.pages.inc --- modules/simpletest/simpletest.pages.inc 11 Sep 2009 01:53:29 -0000 1.16 +++ modules/simpletest/simpletest.pages.inc 11 Sep 2009 20:17:25 -0000 @@ -7,414 +7,6 @@ */ /** - * List tests arranged in groups that can be selected and run. - */ -function simpletest_test_form() { - $form = array(); - - $form['tests'] = array( - '#type' => 'fieldset', - '#title' => t('Tests'), - '#description' => t('Select the test(s) or test group(s) you would like to run, and click Run tests.'), - ); - - $form['tests']['table'] = array( - '#theme' => 'simpletest_test_table', - ); - - // Generate the list of tests arranged by group. - $groups = simpletest_test_get_all(); - foreach ($groups as $group => $tests) { - $form['tests']['table'][$group] = array( - '#collapsed' => TRUE, - ); - - foreach ($tests as $class => $info) { - $form['tests']['table'][$group][$class] = array( - '#type' => 'checkbox', - '#title' => $info['name'], - '#description' => $info['description'], - ); - } - } - - // Operation buttons. - $form['tests']['op'] = array( - '#type' => 'submit', - '#value' => t('Run tests'), - ); - $form['clean'] = array( - '#type' => 'fieldset', - '#collapsible' => FALSE, - '#collapsed' => FALSE, - '#title' => t('Clean test environment'), - '#description' => t('Remove tables with the prefix "simpletest" and temporary directories that are left over from tests that crashed. This is intended for developers when creating tests.'), - ); - $form['clean']['op'] = array( - '#type' => 'submit', - '#value' => t('Clean environment'), - '#submit' => array('simpletest_clean_environment'), - ); - - return $form; -} - -/** - * Theme the test list generated by simpletest_test_form() into a table. - * - * @param $table Form array that represent a table. - * @return HTML output. - */ -function theme_simpletest_test_table($table) { - drupal_add_css(drupal_get_path('module', 'simpletest') . '/simpletest.css'); - drupal_add_js(drupal_get_path('module', 'simpletest') . '/simpletest.js'); - - // Create header for test selection table. - $header = array( - theme('table_select_header_cell'), - array('data' => t('Test'), 'class' => array('simpletest_test')), - array('data' => t('Description'), 'class' => array('simpletest_description')), - ); - - // Define the images used to expand/collapse the test groups. - $js = array( - 'images' => array( - theme('image', 'misc/menu-collapsed.png', 'Expand', 'Expand'), - theme('image', 'misc/menu-expanded.png', 'Collapsed', 'Collapsed'), - ), - ); - - // Cycle through each test group and create a row. - $rows = array(); - foreach (element_children($table) as $key) { - $element = &$table[$key]; - $row = array(); - - // Make the class name safe for output on the page by replacing all - // non-word/decimal characters with a dash (-). - $test_class = strtolower(trim(preg_replace("/[^\w\d]/", "-", $key))); - - // Select the right "expand"/"collapse" image, depending on whether the - // category is expanded (at least one test selected) or not. - $collapsed = !empty($element['#collapsed']); - $image_index = $collapsed ? 0 : 1; - - // Place-holder for checkboxes to select group of tests. - $row[] = array('id' => $test_class, 'class' => array('simpletest-select-all')); - - // Expand/collapse image and group title. - $row[] = array( - 'data' => '
 ' . - '', - 'style' => 'font-weight: bold;' - ); - - $row[] = ' '; - - $rows[] = array('data' => $row, 'class' => array('simpletest-group')); - - // Add individual tests to group. - $current_js = array( - 'testClass' => $test_class . '-test', - 'testNames' => array(), - 'imageDirection' => $image_index, - 'clickActive' => FALSE, - ); - - // Sorting $element by children's #title attribute instead of by class name. - uasort($element, '_simpletest_sort_by_title'); - - // Cycle through each test within the current group. - foreach (element_children($element) as $test_name) { - $test = $element[$test_name]; - $row = array(); - - $current_js['testNames'][] = 'edit-' . $test_name; - - // Store test title and description so that checkbox won't render them. - $title = $test['#title']; - $description = $test['#description']; - - unset($test['#title']); - unset($test['#description']); - - // Test name is used to determine what tests to run. - $test['#name'] = $test_name; - - $row[] = drupal_render($test); - $row[] = theme('indentation', 1) . ''; - $row[] = '
' . $description . '
'; - - $rows[] = array('data' => $row, 'class' => array($test_class . '-test', ($collapsed ? 'js-hide' : ''))); - } - $js['simpletest-test-group-' . $test_class] = $current_js; - unset($table[$key]); - } - - // Add js array of settings. - drupal_add_js(array('simpleTest' => $js), 'setting'); - - if (empty($rows)) { - return '' . t('No tests to display.') . ''; - } - else { - return theme('table', $header, $rows, array('id' => 'simpletest-form-table')); - } -} - -/** - * Sort element by title instead of by class name. - */ -function _simpletest_sort_by_title($a, $b) { - // This is for parts of $element that are not an array. - if (!isset($a['#title']) || !isset($b['#title'])) { - return 1; - } - - return strcasecmp($a['#title'], $b['#title']); -} - -/** - * Run selected tests. - */ -function simpletest_test_form_submit($form, &$form_state) { - // Get list of tests. - $tests_list = array(); - foreach ($form_state['values'] as $class_name => $value) { - if (class_exists($class_name) && $value === 1) { - $tests_list[] = $class_name; - } - } - if (count($tests_list) > 0 ) { - simpletest_run_tests($tests_list, 'drupal'); - } - else { - drupal_set_message(t('No test(s) selected.'), 'error'); - } -} - -/** - * Test results form for $test_id. - */ -function simpletest_result_form(&$form_state, $test_id) { - $form = array(); - - // Make sure there are test results to display and a re-run is not being performed. - $results = array(); - if (is_numeric($test_id) && !$results = simpletest_result_get($test_id)) { - drupal_set_message(t('No test results to display.'), 'error'); - drupal_goto('admin/config/development/testing'); - return $form; - } - - // Load all classes and include CSS. - drupal_add_css(drupal_get_path('module', 'simpletest') . '/simpletest.css'); - - // Keep track of which test cases passed or failed. - $filter = array( - 'pass' => array(), - 'fail' => array(), - ); - - // Summary result fieldset. - $form['result'] = array( - '#type' => 'fieldset', - '#title' => t('Results'), - ); - $form['result']['summary'] = $summary = array( - '#theme' => 'simpletest_result_summary', - '#pass' => 0, - '#fail' => 0, - '#exception' => 0, - '#debug' => 0, - ); - - // Cycle through each test group. - $header = array(t('Message'), t('Group'), t('Filename'), t('Line'), t('Function'), array('colspan' => 2, 'data' => t('Status'))); - $form['result']['results'] = array(); - foreach ($results as $group => $assertions) { - // Create group fieldset with summary information. - $info = call_user_func(array($group, 'getInfo')); - $form['result']['results'][$group] = array( - '#type' => 'fieldset', - '#title' => $info['name'], - '#description' => $info['description'], - '#collapsible' => TRUE, - ); - $form['result']['results'][$group]['summary'] = $summary; - $group_summary = &$form['result']['results'][$group]['summary']; - - // Create table of assertions for the group. - $rows = array(); - foreach ($assertions as $assertion) { - $row = array(); - $row[] = $assertion->message; - $row[] = $assertion->message_group; - $row[] = basename($assertion->file); - $row[] = $assertion->line; - $row[] = $assertion->function; - $row[] = simpletest_result_status_image($assertion->status); - - $class = 'simpletest-' . $assertion->status; - if ($assertion->message_group == 'Debug') { - $class = 'simpletest-debug'; - } - $rows[] = array('data' => $row, 'class' => array($class)); - - $group_summary['#' . $assertion->status]++; - $form['result']['summary']['#' . $assertion->status]++; - } - $form['result']['results'][$group]['table'] = array( - '#theme' => 'table', - '#header' => $header, - '#rows' => $rows, - ); - - // Set summary information. - $group_summary['#ok'] = $group_summary['#fail'] + $group_summary['#exception'] == 0; - $form['result']['results'][$group]['#collapsed'] = $group_summary['#ok'] && !$group_summary['#debug']; - - // Store test group (class) as for use in filter. - $filter[$group_summary['#ok'] ? 'pass' : 'fail'][] = $group; - } - - // Overal summary status. - $form['result']['summary']['#ok'] = $form['result']['summary']['#fail'] + $form['result']['summary']['#exception'] == 0; - - // Actions. - $form['#action'] = url('admin/config/development/testing/results/re-run'); - $form['action'] = array( - '#type' => 'fieldset', - '#title' => t('Actions'), - '#attributes' => array('class' => array('container-inline')), - '#weight' => -11, - ); - - $form['action']['filter'] = array( - '#type' => 'select', - '#title' => 'Filter', - '#options' => array( - 'all' => t('All (@count)', array('@count' => count($filter['pass']) + count($filter['fail']))), - 'pass' => t('Pass (@count)', array('@count' => count($filter['pass']))), - 'fail' => t('Fail (@count)', array('@count' => count($filter['fail']))), - ), - ); - $form['action']['filter']['#default_value'] = ($filter['fail'] ? 'fail' : 'all'); - - // Catagorized test classes for to be used with selected filter value. - $form['action']['filter_pass'] = array( - '#type' => 'hidden', - '#default_value' => implode(',', $filter['pass']), - ); - $form['action']['filter_fail'] = array( - '#type' => 'hidden', - '#default_value' => implode(',', $filter['fail']), - ); - - $form['action']['op'] = array( - '#type' => 'submit', - '#value' => t('Run tests'), - ); - - $form['action']['return'] = array( - '#markup' => l(t('Return to list'), 'admin/config/development/testing'), - ); - - if (is_numeric($test_id)) { - simpletest_clean_results_table($test_id); - } - - return $form; -} - -/** - * Re-run the tests that match the filter. - */ -function simpletest_result_form_submit($form, &$form_state) { - $pass = $form_state['values']['filter_pass'] ? explode(',', $form_state['values']['filter_pass']) : array(); - $fail = $form_state['values']['filter_fail'] ? explode(',', $form_state['values']['filter_fail']) : array(); - - if ($form_state['values']['filter'] == 'all') { - $classes = array_merge($pass, $fail); - } - else if ($form_state['values']['filter'] == 'pass') { - $classes = $pass; - } - else { - $classes = $fail; - } - - if (!$classes) { - $form_state['redirect'] = 'admin/config/development/testing'; - return; - } - - $form_state_execute = array('values' => array()); - foreach ($classes as $class) { - $form_state_execute['values'][$class] = 1; - } - - simpletest_test_form_submit(array(), $form_state_execute); -} - -/** - * Add wrapper div with class based on summary status. - * - * @return HTML output. - */ -function theme_simpletest_result_summary($form) { - return '
' . _simpletest_format_summary_line($form) . '
'; -} - -/** - * Get test results for $test_id. - * - * @param $test_id The test_id to retrieve results of. - * @return Array of results grouped by test_class. - */ -function simpletest_result_get($test_id) { - $results = db_select('simpletest') - ->fields('simpletest') - ->condition('test_id', $test_id) - ->orderBy('test_class') - ->orderBy('message_id') - ->execute(); - - $test_results = array(); - foreach ($results as $result) { - if (!isset($test_results[$result->test_class])) { - $test_results[$result->test_class] = array(); - } - $test_results[$result->test_class][] = $result; - } - return $test_results; -} - -/** - * Get the appropriate image for the status. - * - * @param $status Status string, either: pass, fail, exception. - * @return HTML image or false. - */ -function simpletest_result_status_image($status) { - // $map does not use drupal_static() as its value never changes. - static $map; - - if (!isset($map)) { - $map = array( - 'pass' => theme('image', 'misc/watchdog-ok.png', t('Pass')), - 'fail' => theme('image', 'misc/watchdog-error.png', t('Fail')), - 'exception' => theme('image', 'misc/watchdog-warning.png', t('Exception')), - 'debug' => theme('image', 'misc/watchdog-warning.png', t('Debug')), - ); - } - if (isset($map[$status])) { - return $map[$status]; - } - return FALSE; -} - -/** * Provides settings form for SimpleTest variables. */ function simpletest_settings_form(&$form_state) { Index: modules/simpletest/simpletest.js.inc =================================================================== RCS file: modules/simpletest/simpletest.js.inc diff -N modules/simpletest/simpletest.js.inc --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ modules/simpletest/simpletest.js.inc 1 Jan 1970 00:00:00 -0000 @@ -0,0 +1,173 @@ + $_SESSION['test_class']), 'setting'); + $form_state = array(); + return simpletest_test_form($form_state, simpletest_js_get_all()); +} + +/** + * Ensure that at least one tests is selected. + */ +function simpletest_js_form_validate($form, &$form_state) { + $valid = FALSE; + foreach ($form_state['values'] as $class_name => $value) { + if ($value === 1) { + $valid = TRUE; + } + } + + if ($form_state['clicked_button']['#value'] == t('Run tests') && !$valid) { + form_set_error('op', t('No test(s) selected.')); + } +} + +/** + * Initiate batch process to run selected tests. + */ +function simpletest_js_form_submit($form, &$form_state) { + $tests = array(); + foreach ($form_state['values'] as $class_name => $value) { + if ($value === 1) { + $tests[] = $class_name; + } + } + + simpletest_test_run('js', $tests); +} + +/** + * Get a list of all of the JS tests provided by the system. + * + * If tests are not cached the goto simpletest/js/collect where javascript will + * be invoked that will send back all test information to the server. + * + * @see simpletest_php_get_all() + */ +function simpletest_js_get_all() { + $groups = &drupal_static(__FUNCTION__, array()); + + if (!$groups) { + if ($cache = cache_get('simpletest_js', 'cache')) { + $groups = $cache->data; + } + else { + drupal_goto('simpletest/js/collect'); + } + } + + return $groups; +} + +/** + * Collect the list of JS tests. + */ +function simpletest_js_collect() { + // TODO Temporary. + $groups = array(); + $groups['System'] = array( + 'checkPlain' => array( + 'name' => 'Check plain', + 'description' => 'Tests the Drupal.checkPlain() JavaScript function for properly escaping HTML.', + 'group' => 'System', + ), + 't' => array( + 'name' => 'Translation', + 'description' => 'Tests the basic translation functionality of the Drupal.t() function, including the proper handling of variable strings.', + 'group' => 'System', + ), + 'behaviors' => array( + 'name' => 'JavaScript behaviors', + 'description' => 'Tests the functionality of Drupal behaviors to make sure it allows JavaScript files to attach and detach behaviors in different contexts.', + 'group' => 'System', + ), + ); + cache_set('simpletest_js', $groups); + + drupal_goto('admin/config/development/testing/js'); +} + +/** + * Callback that registers javascript tests. + */ +function simpletest_js_register() { + $groups = array(); + if ($cache = cache_get('simpletest_js', 'cache')) { + $groups = $cache->data; + } + + // TODO Add new data. + + // Sort the groups by key and the individual tests by name. + uksort($groups, 'strnatcasecmp'); + foreach ($groups as $group => &$tests) { + uksort($tests, 'simpletest_sort_by_name'); + } + + cache_set('simpletest_js', $groups); +} + +/** + * Set up the QUnit testing framework. + */ +function simpletest_js_qunit() { + drupal_add_library('simpletest', 'qunit'); + $directories = simpletest_js_tests_load(); + + drupal_add_js(array('modules' => $directories), 'setting'); + drupal_add_js(array('activeTest' => $_SESSION['test_class']), 'setting'); + + echo theme('simpletest_qunit', drupal_get_js(), drupal_get_css()); +} + +/** + * Load all the JS tests provided. + * + * @return + * List of directories in which the tests reside. + */ +function simpletest_js_tests_load() { + $directories = array(); + foreach (_system_get_module_data() as $module => $info) { + $found = FALSE; + $directory = dirname($info->uri); + + $file = "$directory/$module.test.js"; + if (is_file($file)) { + drupal_add_js($file); + $found = TRUE; + } + + if (is_dir("$directory/tests")) { + foreach (file_scan_directory("$directory/tests", '/\.test\.js$/') as $file) { + drupal_add_js($file->uri); + $found = TRUE; + } + } + + if ($found) { + $directories[$module] = $directory; + } + } + + return $directories; +} + +/** + * Record test results from javascript test. + */ +function simpletest_js_record() { + foreach ($_POST['assertions'] as $assertion) { + $status = $assertion['status'] == 'true' ? 'pass' : 'fail'; + DrupalTestCase::insertAssert($_SESSION['test_id'], $_POST['index'], $status, $assertion['message']); + } +} Index: modules/simpletest/tests/drupal.test.js =================================================================== RCS file: modules/simpletest/tests/drupal.test.js diff -N modules/simpletest/tests/drupal.test.js --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ modules/simpletest/tests/drupal.test.js 1 Jan 1970 00:00:00 -0000 @@ -0,0 +1,136 @@ +// $Id: drupal.test.js,v 1.3 2009/09/04 09:42:20 cwgordon7 Exp $ + +(function($) { + +/** + * Test the Drupal.checkPlain function. + */ +Drupal.tests.checkPlain = { + getInfo: function() { + return { + name: 'Check plain', + description: 'Tests the Drupal.checkPlain() JavaScript function for properly escaping HTML.', + group: 'System' + }; + }, + test: function() { + expect(9); + + // Test basic strings. + equals(Drupal.checkPlain('test'), 'test', Drupal.t("Nothing gets replaced that doesn't need to be replaced with their escaped equivalent.")); + equals(Drupal.checkPlain('"test'), '"test', Drupal.t('Quotes are replaced with their escaped equivalent.')); + equals(Drupal.checkPlain('Test&1'), 'Test&1', Drupal.t('Ampersands are replaced with their escaped equivalent.')); + equals(Drupal.checkPlain('Test>test'), 'Test>test', Drupal.t('Greater-than signs are replaced with their escaped equivalent.')); + equals(Drupal.checkPlain('TestStuff'), '<tagname property="value">Stuff</tagname>', Drupal.t('Full HTML tags are replaced with their escaped equivalent.')); + equals(Drupal.checkPlain('Test "&".'), 'Test "&".', Drupal.t('A string with both quotes and ampersands replaces those entities with their escaped equivalents.')); + } +}; + +/** + * Tests Drupal.t(). + */ +Drupal.tests.t = { + getInfo: function() { + return { + name: 'Translation', + description: 'Tests the basic translation functionality of the Drupal.t() function, including the proper handling of variable strings.', + group: 'System' + }; + }, + setup: function() { + this.originalLocale = Drupal.locale; + Drupal.locale = { + 'strings': { + 'Translation 1': '1 noitalsnarT', + 'Translation with a @placeholder': '@placeholder a with Translation', + 'Translation with another %placeholder': '%placeholder in another translation', + 'Literal !placeholder': 'A literal !placeholder', + 'Test unspecified placeholder': 'Unspecified placeholder test' + } + }; + }, + teardown: function() { + Drupal.locale = this.originalLocale; + }, + test: function() { + expect(9); + + var html = 'Apples & Oranges'; + var escaped = '<tag attribute="value">Apples & Oranges</tag>'; + + // Test placeholders. + equals(Drupal.t('Hello world! @html', {'@html': html}), 'Hello world! ' + escaped, Drupal.t('The "@" placeholder escapes the variable.')); + equals(Drupal.t('Hello world! %html', {'%html': html}), 'Hello world! ' + escaped + '', Drupal.t('The "%" placeholder escapes the variable and themes it as a placeholder.')); + equals(Drupal.t('Hello world! !html', {'!html': html}), 'Hello world! ' + html, Drupal.t('The "!" placeholder passes the variable through as-is.')); + equals(Drupal.t('Hello world! html', {'html': html}), 'Hello world! ' + escaped + '', Drupal.t('Other placeholders act as "%" placeholders do.')); + + // Test actual translations. + equals(Drupal.t('Translation 1'), '1 noitalsnarT', Drupal.t('Basic translations work.')); + equals(Drupal.t('Translation with a @placeholder', {'@placeholder': ''}), '<script>alert("xss")</script> a with Translation', Drupal.t('Translations with the "@" placeholder work.')); + equals(Drupal.t('Translation with another %placeholder', {'%placeholder': ''}), '<script>alert("xss")</script> in another translation', Drupal.t('Translations with the "%" placeholder work.')); + equals(Drupal.t('Literal !placeholder', {'!placeholder': ''}), 'A literal ', Drupal.t('Translations with the "!" placeholder work.')); + equals(Drupal.t('Test unspecified placeholder', {'placeholder': ''}), 'Unspecified <script>alert("xss")</script> test', Drupal.t('Translations with unspecified placeholders work.')); + }, +}; + +/** + * Tests Drupal.attachBehaviors() and Drupal.detachBehaviors(). + */ +Drupal.tests.behaviors = { + getInfo: function() { + return { + name: 'JavaScript behaviors', + description: 'Tests the functionality of Drupal behaviors to make sure it allows JavaScript files to attach and detach behaviors in different contexts.', + group: 'System' + }; + }, + setup: function() { + this.originalBehaviors = Drupal.behaviors; + var attachIndex = 0; + var detachIndex = 0; + Drupal.behaviors = { + testBehavior: { + attach: function(context, settings) { + attachIndex++; + equals(context, 'Attach context ' + attachIndex, Drupal.t('Attach context matches passed context.')); + equals(settings, 'Attach settings ' + attachIndex, Drupal.t('Attach settings match passed settings.')); + }, + detach: function(context, settings) { + detachIndex++; + equals(context, 'Detach context ' + detachIndex, Drupal.t('Detach context matches passed context.')); + equals(settings, 'Detach settings ' + detachIndex, Drupal.t('Detach settings match passed settings.')); + } + } + }; + }, + teardown: function() { + Drupal.behaviors = this.originalBehaviors; + }, + test: function() { + expect(8); + + // Test attaching behaviors. + Drupal.attachBehaviors('Attach context 1', 'Attach settings 1'); + + // Test attaching behaviors again. + Drupal.attachBehaviors('Attach context 2', 'Attach settings 2'); + + // Test detaching behaviors. + Drupal.detachBehaviors('Detach context 1', 'Detach settings 1'); + + // Try detaching behaviors again. + Drupal.detachBehaviors('Detach context 2', 'Detach settings 2'); + }, + teardown: function() { + Drupal.behaviors = this.originalBehaviors; + } +}; + +})(jQuery); Index: modules/simpletest/simpletest.php.inc =================================================================== RCS file: modules/simpletest/simpletest.php.inc diff -N modules/simpletest/simpletest.php.inc --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ modules/simpletest/simpletest.php.inc 1 Jan 1970 00:00:00 -0000 @@ -0,0 +1,120 @@ + $value) { + if ($value === 1 && !class_exists($class_name)) { + form_set_error($class_name, t('%class is not a valid test class.', array('%class' => $class_name))); + } + elseif ($value === 1) { + $valid = TRUE; + } + } + + if ($form_state['clicked_button']['#value'] == t('Run tests') && !$valid) { + form_set_error('op', t('No test(s) selected.')); + } +} + +/** + * Initiate batch process to run selected tests. + */ +function simpletest_php_form_submit($form, &$form_state) { + $tests = array(); + foreach ($form_state['values'] as $class_name => $value) { + if ($value === 1) { + $tests[] = $class_name; + } + } + + simpletest_test_run('php', $tests); +} + +/** + * Get a list of all of the PHP tests provided by the system. + * + * The list of test classes is loaded from the registry where it looks for + * files ending in ".test". Once loaded the test list is cached and stored in + * a static variable. In order to list tests provided by disabled modules + * hook_registry_files_alter() is used to forcefully add them to the registry. + * + * @return + * An array of tests keyed with the groups specified in each of the tests + * getInfo() method and then keyed by the test class. An example of the array + * structure is provided below. + * + * @code + * $groups['Blog'] => array( + * 'BlogTestCase' => array( + * 'name' => 'Blog functionality', + * 'description' => 'Create, view, edit, delete, ...', + * 'group' => 'Blog', + * ), + * ); + * @endcode + * @see simpletest_registry_files_alter() + */ +function simpletest_php_get_all() { + $groups = &drupal_static(__FUNCTION__); + + if (!$groups) { + // Load test information from cache if available, otherwise retrieve the + // information from each tests getInfo() method. + if ($cache = cache_get('simpletest_php', 'cache')) { + $groups = $cache->data; + } + else { + // Select all clases in files ending with .test. + $classes = db_select('registry') + ->fields('registry', array('name')) + ->condition('type', 'class') + ->condition('filename', '%.test', 'LIKE') + ->execute(); + + $groups = array(); + + // Check that each class has a getInfo() method and store the information + // in an array keyed with the group specified in the test information. + foreach ($classes as $class) { + $class = $class->name; + if (class_exists($class) && method_exists($class, 'getInfo')) { + // Valid test class, retrieve test information. + $info = call_user_func(array($class, 'getInfo')); + + // Initialize test groups. + if (!isset($groups[$info['group']])) { + $groups[$info['group']] = array(); + } + $groups[$info['group']][$class] = $info; + } + } + + // Sort the groups by key and the individual tests by name. + uksort($groups, 'strnatcasecmp'); + foreach ($groups as $group => &$tests) { + uksort($tests, 'simpletest_sort_by_name'); + } + + cache_set('simpletest_php', $groups); + } + } + return $groups; +} Index: modules/simpletest/simpletest.inc =================================================================== RCS file: modules/simpletest/simpletest.inc diff -N modules/simpletest/simpletest.inc --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ modules/simpletest/simpletest.inc 1 Jan 1970 00:00:00 -0000 @@ -0,0 +1,616 @@ + 'fieldset', + '#title' => t('Tests'), + '#description' => t('Select the test(s) or test group(s) you would like to run, and click Run tests.'), + ); + $form['tests']['table'] = array( + '#theme' => 'simpletest_test_table', + ); + + // Generate the list of tests arranged by group. + foreach ($groups as $group => $tests) { + if (!isset($form['tests']['table'][$group])) { + $form['tests']['table'][$group] = array( + '#collapsed' => TRUE, + ); + } + + foreach ($tests as $key => $info) { + $form['tests']['table'][$group][$key] = array( + '#type' => 'checkbox', + '#title' => $info['name'], + '#description' => $info['description'], + ); + } + } + + // Operation buttons. + $form['tests']['op'] = array( + '#type' => 'submit', + '#value' => t('Run tests'), + ); + $form['clean'] = array( + '#type' => 'fieldset', + '#collapsible' => FALSE, + '#collapsed' => FALSE, + '#title' => t('Clean test environment'), + '#description' => t('Remove tables with the prefix "simpletest" and temporary directories that are left over from tests that crashed. This is intended for developers when creating tests.'), + ); + $form['clean']['op'] = array( + '#type' => 'submit', + '#value' => t('Clean environment'), + '#submit' => array('simpletest_clean_environment'), + ); + + return $form; +} + +/** + * Theme the test list generated by simpletest_test_form() into a table. + * + * @param $table Form array that represent a table. + * @return HTML output. + */ +function theme_simpletest_test_table($table) { + drupal_add_css(drupal_get_path('module', 'simpletest') . '/simpletest.css'); + drupal_add_js(drupal_get_path('module', 'simpletest') . '/simpletest.js'); + + // Create header for test selection table. + $header = array( + theme('table_select_header_cell'), + array('data' => t('Test'), 'class' => array('simpletest_test')), + array('data' => t('Description'), 'class' => array('simpletest_description')), + ); + + // Define the images used to expand/collapse the test groups. + $js = array( + 'images' => array( + theme('image', 'misc/menu-collapsed.png', 'Expand', 'Expand'), + theme('image', 'misc/menu-expanded.png', 'Collapsed', 'Collapsed'), + ), + ); + + // Cycle through each test group and create a row. + $rows = array(); + foreach (element_children($table) as $key) { + $element = &$table[$key]; + $row = array(); + + // Make the class name safe for output on the page by replacing all + // non-word/decimal characters with a dash (-). + $test_class = strtolower(trim(preg_replace("/[^\w\d]/", "-", $key))); + + // Select the right "expand"/"collapse" image, depending on whether the + // category is expanded (at least one test selected) or not. + $collapsed = !empty($element['#collapsed']); + $image_index = $collapsed ? 0 : 1; + + // Place-holder for checkboxes to select group of tests. + $row[] = array('id' => $test_class, 'class' => array('simpletest-select-all')); + + // Expand/collapse image and group title. + $row[] = array( + 'data' => '
 ' . + '', + 'style' => 'font-weight: bold;' + ); + + $row[] = ' '; + + $rows[] = array('data' => $row, 'class' => array('simpletest-group')); + + // Add individual tests to group. + $current_js = array( + 'testClass' => $test_class . '-test', + 'testNames' => array(), + 'imageDirection' => $image_index, + 'clickActive' => FALSE, + ); + + // Cycle through each test within the current group. + foreach (element_children($element) as $test_name) { + $test = $element[$test_name]; + $row = array(); + + $current_js['testNames'][] = 'edit-' . $test_name; + + // Store test title and description so that checkbox won't render them. + $title = $test['#title']; + $description = $test['#description']; + + unset($test['#title']); + unset($test['#description']); + + // Test name is used to determine what tests to run. + $test['#name'] = $test_name; + + $row[] = drupal_render($test); + $row[] = theme('indentation', 1) . ''; + $row[] = '
' . $description . '
'; + + $rows[] = array('data' => $row, 'class' => array($test_class . '-test', ($collapsed ? 'js-hide' : ''))); + } + $js['simpletest-test-group-' . $test_class] = $current_js; + unset($table[$key]); + } + + // Add js array of settings. + drupal_add_js(array('simpleTest' => $js), 'setting'); + + if (empty($rows)) { + return '
' . t('No tests to display.') . '
'; + } + else { + return theme('table', $header, $rows, array('id' => 'simpletest-form-table')); + } +} + +function simpletest_sort_by_name($a, $b) { + return strcasecmp($a['name'], $b['name']); +} + +/** + * Set up batch process to run tests. + * + * @param $type + * Type of testing to be conducted, either 'php' or 'js'. + * @param $tests + * List of tests to run. + */ +function simpletest_test_run($type, array $tests) { + cache_clear_all(); + $test_id = $_SESSION['test_id'] = simpletest_test_id_reserve(); + + // Clear out the previous verbose files. + file_unmanaged_delete_recursive(file_directory_path() . '/simpletest/verbose'); + + // Get the info for the first test being run. + $info = simpletest_get_info($type, $tests[0]); + + $batch = array( + 'title' => t('Running tests'), + 'operations' => array( + array('_simpletest_batch_operation', array($test_id, $type, $tests)), + ), + 'finished' => '_simpletest_batch_finished', + 'progress_message' => '', + 'css' => array(drupal_get_path('module', 'simpletest') . '/simpletest.css'), + 'init_message' => t('Processing test @num of @max - %test.', array('%test' => $info['name'], '@num' => '1', '@max' => count($tests))), + ); + batch_set($batch); + + if ($type == 'php') { + module_invoke_all('test_group_started'); + } + else { + $url = url('', array('absolute' => TRUE)) . drupal_get_path('module', 'simpletest') . '/qunit.php'; + $context['progress_message'] = ''; + } + + // Normally, the forms portion of the batch API takes care of calling + // batch_process(), but in the process it saves the whole $form into the + // database (which is huge for the test selection form). + // By calling batch_process() directly, we skip that behavior and ensure + // that we don't exceed the size of data that can be sent to the database + // (max_allowed_packet on MySQL). + batch_process('admin/config/development/testing/results/' . $type . '/' . $test_id); +} + +/** + * Batch operation callback. + */ +function _simpletest_batch_operation($test_id, $type, $tests_init, &$context) { + // Get working values. + if (!isset($context['sandbox']['max'])) { + // First iteration: initialize working values. + $tests = $tests_init; + $context['sandbox']['max'] = count($tests); + $test_results = array('#pass' => 0, '#fail' => 0, '#exception' => 0, '#debug' => 0); + } + else { + // Nth iteration: get the current values where we last stored them. + $tests = $context['sandbox']['tests']; + $test_results = $context['sandbox']['test_results']; + } + $max = $context['sandbox']['max']; + + if ($type == 'php') { + // Run the next test. + $test_class = array_shift($tests); + $test = new $test_class($test_id); + $test->run(); + $results = $test->results; + $size = count($tests); + $info = $test->getInfo(); + + module_invoke_all('test_finished', $test->results); + } + else { + $test_class = array_shift($tests); + $info = simpletest_get_info($type, $test_class); + + $results = simpletest_result_get($test_id); + if ($results && isset($results[$test_class])) { + $results = simpletest_result_summary($results[$test_class]); + + if ($tests) { + $active_class = array_shift($tests); + array_unshift($tests, $active_class); + } + } + else { + array_unshift($tests, $test_class); + $active_class = $test_class; + } + + $size = count($tests); + } + + // Gather results and compose the report. + $test_results[$test_class] = $results; + foreach ($test_results[$test_class] as $key => $value) { + $test_results[$key] += $value; + } + $test_results[$test_class]['#name'] = $info['name']; + $items = array(); + foreach (element_children($test_results) as $class) { + array_unshift($items, '
' . t('@name: @summary', array('@name' => $test_results[$class]['#name'], '@summary' => _simpletest_format_summary_line($test_results[$class]))) . '
'); + } + $context['message'] = t('Processed test @num of @max - %test.', array('%test' => $info['name'], '@num' => $max - $size, '@max' => $max)); + $context['message'] .= '
Overall results: ' . _simpletest_format_summary_line($test_results) . '
'; + $context['message'] .= theme('item_list', $items); + + if ($type == 'js' && $tests) { + // Cannot send test class via GET parameter due to QUnit limitation, so + // store and pass as setting in callback. + $_SESSION['test_class'] = $active_class; + $url = url('', array('absolute' => TRUE)) . drupal_get_path('module', 'simpletest') . '/qunit.php'; + $context['message'] .= ''; + } + + // Save working values for the next iteration. + $context['sandbox']['tests'] = $tests; + $context['sandbox']['test_results'] = $test_results; + // The test_id is the only thing we need to save for the report page. + $context['results']['test_id'] = $test_id; + + // Multistep processing: report progress. + $context['finished'] = 1 - $size / $max; +} + +/** + * Batch finished callback. + */ +function _simpletest_batch_finished($success, $results, $operations, $elapsed) { + if ($success) { + drupal_set_message(t('The test run finished in @elapsed.', array('@elapsed' => $elapsed))); + } + else { + // Use the test_id passed as a parameter to _simpletest_batch_operation(). + $test_id = $operations[0][1][0]; + + // Retrieve the last database prefix used for testing and the last test + // class that was run from. Use the information to read the lgo file + // in case any fatal errors caused the test to crash. + list($last_prefix, $last_test_class) = simpletest_last_test_get($test_id); + simpletest_log_read($test_id, $last_prefix, $last_test_class); + + + drupal_set_message(t('The test run did not successfully finish.'), 'error'); + drupal_set_message(t('Please use the Clean environment button to clean-up temporary files and tables.'), 'warning'); + } + module_invoke_all('test_group_finished'); +} + +/** + * Get test info. + * + * @param $type + * Type of testing to be conducted, either 'php' or 'js'. + * @param $test_class + * The test class to get info for. + * @return + * Test info or NULL. + */ +function simpletest_get_info($type, $test_class) { + $function = 'simpletest_' . $type . '_get_all'; + $groups = $function(); + + foreach ($groups as $group => $tests) { + if (isset($tests[$test_class])) { + return $tests[$test_class]; + } + } + return NULL; +} + +/** + * Reserve a test ID. + * + * @return + * Test ID. + */ +function simpletest_test_id_reserve() { + return db_insert('simpletest_test_id') + ->useDefaults(array('test_id')) + ->execute(); +} + +/** + * Get test results for $test_id. + * + * @param $test_id The test_id to retrieve results of. + * @return Array of results grouped by test_class. + */ +function simpletest_result_get($test_id) { + $results = db_select('simpletest') + ->fields('simpletest') + ->condition('test_id', $test_id) + ->orderBy('test_class') + ->orderBy('message_id') + ->execute(); + + $test_results = array(); + foreach ($results as $result) { + if (!isset($test_results[$result->test_class])) { + $test_results[$result->test_class] = array(); + } + $test_results[$result->test_class][] = $result; + } + return $test_results; +} + +/** + * Get summary count of assertions. + * + * @param $assertions + * List of assertions. + * @return + * Summary count of each status type as form attributes. + */ +function simpletest_result_summary(array $assertions) { + $summary = array('#pass' => 0, '#fail' => 0, '#exception' => 0, '#debug' => 0); + foreach ($assertions as $assertion) { + $summary['#' . $assertion->status]++; + } + return $summary; +} + +/** + * Format a line summary. + * + * @param $summary + * Array of summary information. + * @return + * String summary. + */ +function _simpletest_format_summary_line($summary) { + $args = array( + '@pass' => format_plural(isset($summary['#pass']) ? $summary['#pass'] : 0, '1 pass', '@count passes'), + '@fail' => format_plural(isset($summary['#fail']) ? $summary['#fail'] : 0, '1 fail', '@count fails'), + '@exception' => format_plural(isset($summary['#exception']) ? $summary['#exception'] : 0, '1 exception', '@count exceptions'), + ); + if (!$summary['#debug']) { + return t('@pass, @fail, and @exception', $args); + } + $args['@debug'] = format_plural(isset($summary['#debug']) ? $summary['#debug'] : 0, '1 debug message', '@count debug messages'); + return t('@pass, @fail, @exception, and @debug', $args); +} + +/** + * Test results form for $test_id. + */ +function simpletest_result_form(&$form_state, $type, $test_id) { + $form = array(); + + if ($type != 'php' && $type != 'js') { + drupal_set_message(t('Invalid result type.'), 'error'); + return array(); + } + + // Make sure there are test results to display and a re-run is not being performed. + $results = array(); + if (is_numeric($test_id) && !$results = simpletest_result_get($test_id)) { + drupal_set_message(t('No test results to display.'), 'error'); + drupal_goto('admin/config/development/testing/' . $type); + return $form; + } + + // Load all classes and include CSS. + drupal_add_css(drupal_get_path('module', 'simpletest') . '/simpletest.css'); + + // Keep track of which test cases passed or failed. + $filter = array( + 'pass' => array(), + 'fail' => array(), + ); + + // Summary result fieldset. + $form['result'] = array( + '#type' => 'fieldset', + '#title' => t('Results'), + ); + $form['result']['summary'] = $summary = array( + '#theme' => 'simpletest_result_summary', + '#pass' => 0, + '#fail' => 0, + '#exception' => 0, + '#debug' => 0, + ); + + // Cycle through each test group. + $header = array(t('Message'), t('Group'), t('Filename'), t('Line'), t('Function'), array('colspan' => 2, 'data' => t('Status'))); + $form['result']['results'] = array(); + $javascript_testing = FALSE; + foreach ($results as $group => $assertions) { + // Create group fieldset with summary information. + $info = simpletest_get_info($type, $group); + $form['result']['results'][$group] = array( + '#type' => 'fieldset', + '#title' => $info['name'], + '#description' => $info['description'], + '#collapsible' => TRUE, + ); + $form['result']['results'][$group]['summary'] = $summary; + $group_summary = &$form['result']['results'][$group]['summary']; + + // Create table of assertions for the group. + $rows = array(); + foreach ($assertions as $assertion) { + $row = array(); + $row[] = $assertion->message; + $row[] = $assertion->message_group; + $row[] = basename($assertion->file); + $row[] = $assertion->line; + $row[] = $assertion->function; + $row[] = simpletest_result_status_image($assertion->status); + + $class = 'simpletest-' . $assertion->status; + if ($assertion->message_group == 'Debug') { + $class = 'simpletest-debug'; + } + $rows[] = array('data' => $row, 'class' => array($class)); + + $group_summary['#' . $assertion->status]++; + $form['result']['summary']['#' . $assertion->status]++; + } + $form['result']['results'][$group]['table'] = array( + '#theme' => 'table', + '#header' => $header, + '#rows' => $rows, + ); + + // Set summary information. + $group_summary['#ok'] = $group_summary['#fail'] + $group_summary['#exception'] == 0; + $form['result']['results'][$group]['#collapsed'] = $group_summary['#ok'] && !$group_summary['#debug']; + + // Store test group (class) as for use in filter. + $filter[$group_summary['#ok'] ? 'pass' : 'fail'][] = $group; + } + + // Overal summary status. + $form['result']['summary']['#ok'] = $form['result']['summary']['#fail'] + $form['result']['summary']['#exception'] == 0; + + // Actions. + $form['#action'] = url('admin/config/development/testing/results/' . $type . '/re-run'); + $form['action'] = array( + '#type' => 'fieldset', + '#title' => t('Actions'), + '#attributes' => array('class' => array('container-inline')), + '#weight' => -11, + ); + + $form['action']['filter'] = array( + '#type' => 'select', + '#title' => 'Filter', + '#options' => array( + 'all' => t('All (@count)', array('@count' => count($filter['pass']) + count($filter['fail']))), + 'pass' => t('Pass (@count)', array('@count' => count($filter['pass']))), + 'fail' => t('Fail (@count)', array('@count' => count($filter['fail']))), + ), + '#access' => !$javascript_testing, + ); + $form['action']['filter']['#default_value'] = ($filter['fail'] ? 'fail' : 'all'); + + // Catagorized test classes for to be used with selected filter value. + $form['action']['filter_pass'] = array( + '#type' => 'hidden', + '#default_value' => implode(',', $filter['pass']), + ); + $form['action']['filter_fail'] = array( + '#type' => 'hidden', + '#default_value' => implode(',', $filter['fail']), + ); + + $form['action']['op'] = array( + '#type' => 'submit', + '#value' => t('Run tests'), + ); + + $form['action']['return'] = array( + '#markup' => l(t('Return to list'), 'admin/config/development/testing/' . $type), + ); + + if (is_numeric($test_id)) { + simpletest_clean_results_table($test_id); + } + + return $form; +} + +/** + * Re-run the tests that match the filter. + */ +function simpletest_result_form_submit($form, &$form_state) { + $pass = $form_state['values']['filter_pass'] ? explode(',', $form_state['values']['filter_pass']) : array(); + $fail = $form_state['values']['filter_fail'] ? explode(',', $form_state['values']['filter_fail']) : array(); + + if ($form_state['values']['filter'] == 'all') { + $classes = array_merge($pass, $fail); + } + else if ($form_state['values']['filter'] == 'pass') { + $classes = $pass; + } + else { + $classes = $fail; + } + + if (!$classes) { + $form_state['redirect'] = 'admin/config/development/testing/' . arg(5); + return; + } + + $form_state_execute = array('values' => array()); + foreach ($classes as $class) { + $form_state_execute['values'][$class] = 1; + } + + simpletest_test_run(arg(5), $classes); +} + +/** + * Add wrapper div with class based on summary status. + * + * @return HTML output. + */ +function theme_simpletest_result_summary($form) { + return '
' . _simpletest_format_summary_line($form) . '
'; +} + +/** + * Get the appropriate image for the status. + * + * @param $status Status string, either: pass, fail, exception. + * @return HTML image or false. + */ +function simpletest_result_status_image($status) { + // $map does not use drupal_static() as its value never changes. + static $map; + + if (!isset($map)) { + $map = array( + 'pass' => theme('image', 'misc/watchdog-ok.png', t('Pass')), + 'fail' => theme('image', 'misc/watchdog-error.png', t('Fail')), + 'exception' => theme('image', 'misc/watchdog-warning.png', t('Exception')), + 'debug' => theme('image', 'misc/watchdog-warning.png', t('Debug')), + ); + } + if (isset($map[$status])) { + return $map[$status]; + } + return FALSE; +} Index: modules/simpletest/testrunner.js =================================================================== RCS file: modules/simpletest/testrunner.js diff -N modules/simpletest/testrunner.js --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ modules/simpletest/testrunner.js 1 Jan 1970 00:00:00 -0000 @@ -0,0 +1,803 @@ +/* + * QUnit - jQuery unit testrunner + * + * http://docs.jquery.com/QUnit + * + * Copyright (c) 2008 John Resig, Jörn Zaefferer + * Dual licensed under the MIT (MIT-LICENSE.txt) + * and GPL (GPL-LICENSE.txt) licenses. + * + * $Id: testrunner.js,v 1.3 2009/09/03 12:35:23 cwgordon7 Exp $ + */ + +(function($) { + +// Test for equality any JavaScript type. +// Discussions and reference: http://philrathe.com/articles/equiv +// Test suites: http://philrathe.com/tests/equiv +// Author: Philippe Rathé +var equiv = function () { + + var innerEquiv; // the real equiv function + var callers = []; // stack to decide between skip/abort functions + + + // Determine what is o. + function hoozit(o) { + if (o.constructor === String) { + return "string"; + + } else if (o.constructor === Boolean) { + return "boolean"; + + } else if (o.constructor === Number) { + + if (isNaN(o)) { + return "nan"; + } else { + return "number"; + } + + } else if (typeof o === "undefined") { + return "undefined"; + + // consider: typeof null === object + } else if (o === null) { + return "null"; + + // consider: typeof [] === object + } else if (o instanceof Array) { + return "array"; + + // consider: typeof new Date() === object + } else if (o instanceof Date) { + return "date"; + + // consider: /./ instanceof Object; + // /./ instanceof RegExp; + // typeof /./ === "function"; // => false in IE and Opera, + // true in FF and Safari + } else if (o instanceof RegExp) { + return "regexp"; + + } else if (typeof o === "object") { + return "object"; + + } else if (o instanceof Function) { + return "function"; + } else { + return undefined; + } + } + + // Call the o related callback with the given arguments. + function bindCallbacks(o, callbacks, args) { + var prop = hoozit(o); + if (prop) { + if (hoozit(callbacks[prop]) === "function") { + return callbacks[prop].apply(callbacks, args); + } else { + return callbacks[prop]; // or undefined + } + } + } + + var callbacks = function () { + + // for string, boolean, number and null + function useStrictEquality(b, a) { + if (b instanceof a.constructor || a instanceof b.constructor) { + // to catch short annotaion VS 'new' annotation of a declaration + // e.g. var i = 1; + // var j = new Number(1); + return a == b; + } else { + return a === b; + } + } + + return { + "string": useStrictEquality, + "boolean": useStrictEquality, + "number": useStrictEquality, + "null": useStrictEquality, + "undefined": useStrictEquality, + + "nan": function (b) { + return isNaN(b); + }, + + "date": function (b, a) { + return hoozit(b) === "date" && a.valueOf() === b.valueOf(); + }, + + "regexp": function (b, a) { + return hoozit(b) === "regexp" && + a.source === b.source && // the regex itself + a.global === b.global && // and its modifers (gmi) ... + a.ignoreCase === b.ignoreCase && + a.multiline === b.multiline; + }, + + // - skip when the property is a method of an instance (OOP) + // - abort otherwise, + // initial === would have catch identical references anyway + "function": function () { + var caller = callers[callers.length - 1]; + return caller !== Object && + typeof caller !== "undefined"; + }, + + "array": function (b, a) { + var i; + var len; + + // b could be an object literal here + if ( ! (hoozit(b) === "array")) { + return false; + } + + len = a.length; + if (len !== b.length) { // safe and faster + return false; + } + for (i = 0; i < len; i++) { + if( ! innerEquiv(a[i], b[i])) { + return false; + } + } + return true; + }, + + "object": function (b, a) { + var i; + var eq = true; // unless we can proove it + var aProperties = [], bProperties = []; // collection of strings + + // comparing constructors is more strict than using instanceof + if ( a.constructor !== b.constructor) { + return false; + } + + // stack constructor before traversing properties + callers.push(a.constructor); + + for (i in a) { // be strict: don't ensures hasOwnProperty and go deep + + aProperties.push(i); // collect a's properties + + if ( ! innerEquiv(a[i], b[i])) { + eq = false; + } + } + + callers.pop(); // unstack, we are done + + for (i in b) { + bProperties.push(i); // collect b's properties + } + + // Ensures identical properties name + return eq && innerEquiv(aProperties.sort(), bProperties.sort()); + } + }; + }(); + + innerEquiv = function () { // can take multiple arguments + var args = Array.prototype.slice.apply(arguments); + if (args.length < 2) { + return true; // end transition + } + + return (function (a, b) { + if (a === b) { + return true; // catch the most you can + } else if (a === null || b === null || typeof a === "undefined" || typeof b === "undefined" || hoozit(a) !== hoozit(b)) { + return false; // don't lose time with error prone cases + } else { + return bindCallbacks(a, callbacks, [b, a]); + } + + // apply transition with (1..n) arguments + })(args[0], args[1]) && arguments.callee.apply(this, args.splice(1, args.length -1)); + }; + + return innerEquiv; + +}(); + +var GETParams = $.map( location.search.slice(1).split('&'), decodeURIComponent ), + ngindex = $.inArray("noglobals", GETParams), + noglobals = ngindex !== -1; + +if( noglobals ) + GETParams.splice( ngindex, 1 ); + +var config = { + stats: { + all: 0, + bad: 0 + }, + queue: [], + // block until document ready + blocking: true, + //restrict modules/tests by get parameters + filters: GETParams, + isLocal: !!(window.location.protocol == 'file:') +}; + +// public API as global methods +$.extend(window, { + test: test, + module: module, + expect: expect, + ok: ok, + equals: equals, + start: start, + stop: stop, + reset: reset, + isLocal: config.isLocal, + same: function(a, b, message) { + push(equiv(a, b), a, b, message); + }, + QUnit: { + equiv: equiv, + ok: ok, + done: function(failures, total){}, + log: function(result, message){} + }, + // legacy methods below + isSet: isSet, + isObj: isObj, + compare: function() { + throw "compare is deprecated - use same() instead"; + }, + compare2: function() { + throw "compare2 is deprecated - use same() instead"; + }, + serialArray: function() { + throw "serialArray is deprecated - use jsDump.parse() instead"; + }, + q: q, + t: t, + url: url, + triggerEvent: triggerEvent +}); + +$(window).load(function() { + + if (!$("#header, #banner, #userAgent, #tests").length) { + $('body').prepend( + '

' + document.title + '

' + + '' + + '

' + + '
    ' + ); + } + + $('#userAgent').html(navigator.userAgent); + var head = $('
    ').insertAfter("#userAgent"); + $('').attr("disabled", true).prependTo(head).click(function() { + $('li.pass')[this.checked ? 'hide' : 'show'](); + }); + $('').attr("disabled", true).appendTo(head).click(function() { + $("li.fail:contains('missing test - untested code is broken code')").parent('ol').parent('li.fail')[this.checked ? 'hide' : 'show'](); + }); + $("#filter-missing").after(''); + runTest(); +}); + +function synchronize(callback) { + config.queue.push(callback); + if(!config.blocking) { + process(); + } +} + +function process() { + while(config.queue.length && !config.blocking) { + config.queue.shift()(); + } +} + +function stop(timeout) { + config.blocking = true; + if (timeout) + config.timeout = setTimeout(function() { + QUnit.ok( false, "Test timed out" ); + start(); + }, timeout); +} +function start() { + // A slight delay, to avoid any current callbacks + setTimeout(function() { + if(config.timeout) + clearTimeout(config.timeout); + config.blocking = false; + process(); + }, 13); +} + +function validTest( name ) { + var i = config.filters.length, + run = false; + + if( !i ) + return true; + + while( i-- ){ + var filter = config.filters[i], + not = filter.charAt(0) == '!'; + if( not ) + filter = filter.slice(1); + if( name.indexOf(filter) != -1 ) + return !not; + if( not ) + run = true; + } + return run; +} + +function runTest() { + config.blocking = false; + var started = +new Date; + config.fixture = document.getElementById('main').innerHTML; + config.ajaxSettings = $.ajaxSettings; + synchronize(function() { + $('

    ').html(['Tests completed in ', + +new Date - started, ' milliseconds.
    ', + '', config.stats.all - config.stats.bad, ' tests of ', config.stats.all, ' passed, ', config.stats.bad,' failed.'] + .join('')) + .appendTo("body"); + $("#banner").addClass(config.stats.bad ? "fail" : "pass"); + QUnit.done( config.stats.bad, config.stats.all ); + }); +} + +var pollution; + +function saveGlobal(){ + pollution = [ ]; + + if( noglobals ) + for( var key in window ) + pollution.push(key); +} +function checkPollution( name ){ + var old = pollution; + saveGlobal(); + + if( pollution.length > old.length ){ + ok( false, "Introduced global variable(s): " + diff(old, pollution).join(", ") ); + config.expected++; + } +} + +function diff( clean, dirty ){ + return $.grep( dirty, function(name){ + return $.inArray( name, clean ) == -1; + }); +} + +function test(name, callback) { + if(config.currentModule) + name = config.currentModule + " module: " + name + ""; + var lifecycle = $.extend({ + setup: function() {}, + teardown: function() {} + }, config.moduleLifecycle); + + if ( !validTest(name) ) + return; + + var testEnvironment = {}; + + synchronize(function() { + config.assertions = []; + config.expected = null; + try { + if( !pollution ) + saveGlobal(); + lifecycle.setup.call(testEnvironment); + } catch(e) { + QUnit.ok( false, "Setup failed on " + name + ": " + e.message ); + } + }); + synchronize(function() { + try { + callback.call(testEnvironment); + } catch(e) { + fail("Test " + name + " died, exception and test follows", e, callback); + QUnit.ok( false, "Died on test #" + (config.assertions.length + 1) + ": " + e.message ); + // else next test will carry the responsibility + saveGlobal(); + } + }); + synchronize(function() { + try { + checkPollution(); + lifecycle.teardown.call(testEnvironment); + } catch(e) { + QUnit.ok( false, "Teardown failed on " + name + ": " + e.message ); + } + }); + synchronize(function() { + try { + reset(); + } catch(e) { + fail("reset() failed, following Test " + name + ", exception and reset fn follows", e, reset); + } + + if(config.expected && config.expected != config.assertions.length) { + QUnit.ok( false, "Expected " + config.expected + " assertions, but " + config.assertions.length + " were run" ); + } + + var good = 0, bad = 0; + var ol = $("

      ").hide(); + config.stats.all += config.assertions.length; + for ( var i = 0; i < config.assertions.length; i++ ) { + var assertion = config.assertions[i]; + $("
    1. ").addClass(assertion.result ? "pass" : "fail").text(assertion.message || "(no message)").appendTo(ol); + assertion.result ? good++ : bad++; + } + config.stats.bad += bad; + + var b = $("").html(name + " (" + bad + ", " + good + ", " + config.assertions.length + ")") + .click(function(){ + $(this).next().toggle(); + }) + .dblclick(function(event) { + var target = $(event.target).filter("strong").clone(); + if ( target.length ) { + target.children().remove(); + location.href = location.href.match(/^(.+?)(\?.*)?$/)[1] + "?" + encodeURIComponent($.trim(target.text())); + } + }); + + $("
    2. ").addClass(bad ? "fail" : "pass").append(b).append(ol).appendTo("#tests"); + + if(bad) { + $("#filter-pass").attr("disabled", null); + $("#filter-missing").attr("disabled", null); + } + }); +} + +function fail(message, exception, callback) { + if( typeof console != "undefined" && console.error && console.warn ) { + console.error(message); + console.error(exception); + console.warn(callback.toString()); + } else if (window.opera && opera.postError) { + opera.postError(message, exception, callback.toString); + } +} + +// call on start of module test to prepend name to all tests +function module(name, lifecycle) { + config.currentModule = name; + config.moduleLifecycle = lifecycle; +} + +/** + * Specify the number of expected assertions to gurantee that failed test (no assertions are run at all) don't slip through. + */ +function expect(asserts) { + config.expected = asserts; +} + +/** + * Resets the test setup. Useful for tests that modify the DOM. + */ +function reset() { + $("#main").html( config.fixture ); + $.event.global = {}; + $.ajaxSettings = $.extend({}, config.ajaxSettings); +} + +/** + * Asserts true. + * @example ok( $("a").size() > 5, "There must be at least 5 anchors" ); + */ +function ok(a, msg) { + QUnit.log(a, msg); + + config.assertions.push({ + result: !!a, + message: msg + }); +} + +/** + * Asserts that two arrays are the same + */ +function isSet(a, b, msg) { + function serialArray( a ) { + var r = []; + + if ( a && a.length ) + for ( var i = 0; i < a.length; i++ ) { + var str = a[i].nodeName; + if ( str ) { + str = str.toLowerCase(); + if ( a[i].id ) + str += "#" + a[i].id; + } else + str = a[i]; + r.push( str ); + } + + return "[ " + r.join(", ") + " ]"; + } + var ret = true; + if ( a && b && a.length != undefined && a.length == b.length ) { + for ( var i = 0; i < a.length; i++ ) + if ( a[i] != b[i] ) + ret = false; + } else + ret = false; + QUnit.ok( ret, !ret ? (msg + " expected: " + serialArray(b) + " result: " + serialArray(a)) : msg ); +} + +/** + * Asserts that two objects are equivalent + */ +function isObj(a, b, msg) { + var ret = true; + + if ( a && b ) { + for ( var i in a ) + if ( a[i] != b[i] ) + ret = false; + + for ( i in b ) + if ( a[i] != b[i] ) + ret = false; + } else + ret = false; + + QUnit.ok( ret, msg ); +} + +/** + * Returns an array of elements with the given IDs, eg. + * @example q("main", "foo", "bar") + * @result [
      , , ] + */ +function q() { + var r = []; + for ( var i = 0; i < arguments.length; i++ ) + r.push( document.getElementById( arguments[i] ) ); + return r; +} + +/** + * Asserts that a select matches the given IDs + * @example t("Check for something", "//[a]", ["foo", "baar"]); + * @result returns true if "//[a]" return two elements with the IDs 'foo' and 'baar' + */ +function t(a,b,c) { + var f = $(b); + var s = ""; + for ( var i = 0; i < f.length; i++ ) + s += (s && ",") + '"' + f[i].id + '"'; + isSet(f, q.apply(q,c), a + " (" + b + ")"); +} + +/** + * Add random number to url to stop IE from caching + * + * @example url("data/test.html") + * @result "data/test.html?10538358428943" + * + * @example url("data/test.php?foo=bar") + * @result "data/test.php?foo=bar&10538358345554" + */ +function url(value) { + return value + (/\?/.test(value) ? "&" : "?") + new Date().getTime() + "" + parseInt(Math.random()*100000); +} + +/** + * Checks that the first two arguments are equal, with an optional message. + * Prints out both actual and expected values. + * + * Prefered to ok( actual == expected, message ) + * + * @example equals( $.format("Received {0} bytes.", 2), "Received 2 bytes." ); + * + * @param Object actual + * @param Object expected + * @param String message (optional) + */ +function equals(actual, expected, message) { + push(expected == actual, actual, expected, message); +} + +function push(result, actual, expected, message) { + message = message || (result ? "okay" : "failed"); + QUnit.ok( result, result ? message + ": " + expected : message + ", expected: " + jsDump.parse(expected) + " result: " + jsDump.parse(actual) ); +} + +/** + * Trigger an event on an element. + * + * @example triggerEvent( document.body, "click" ); + * + * @param DOMElement elem + * @param String type + */ +function triggerEvent( elem, type, event ) { + if ( $.browser.mozilla || $.browser.opera ) { + event = document.createEvent("MouseEvents"); + event.initMouseEvent(type, true, true, elem.ownerDocument.defaultView, + 0, 0, 0, 0, 0, false, false, false, false, 0, null); + elem.dispatchEvent( event ); + } else if ( $.browser.msie ) { + elem.fireEvent("on"+type); + } +} + +})(jQuery); + +/** + * jsDump + * Copyright (c) 2008 Ariel Flesler - aflesler(at)gmail(dot)com | http://flesler.blogspot.com + * Licensed under BSD (http://www.opensource.org/licenses/bsd-license.php) + * Date: 5/15/2008 + * @projectDescription Advanced and extensible data dumping for Javascript. + * @version 1.0.0 + * @author Ariel Flesler + * @link {http://flesler.blogspot.com/2008/05/jsdump-pretty-dump-of-any-javascript.html} + */ +(function(){ + function quote( str ){ + return '"' + str.toString().replace(/"/g, '\\"') + '"'; + }; + function literal( o ){ + return o + ''; + }; + function join( pre, arr, post ){ + var s = jsDump.separator(), + base = jsDump.indent(), + inner = jsDump.indent(1); + if( arr.join ) + arr = arr.join( ',' + s + inner ); + if( !arr ) + return pre + post; + return [ pre, inner + arr, base + post ].join(s); + }; + function array( arr ){ + var i = arr.length, ret = Array(i); + this.up(); + while( i-- ) + ret[i] = this.parse( arr[i] ); + this.down(); + return join( '[', ret, ']' ); + }; + + var reName = /^function (\w+)/; + + var jsDump = window.jsDump = { + parse:function( obj, type ){//type is used mostly internally, you can fix a (custom)type in advance + var parser = this.parsers[ type || this.typeOf(obj) ]; + type = typeof parser; + + return type == 'function' ? parser.call( this, obj ) : + type == 'string' ? parser : + this.parsers.error; + }, + typeOf:function( obj ){ + var type = typeof obj, + f = 'function';//we'll use it 3 times, save it + return type != 'object' && type != f ? type : + !obj ? 'null' : + obj.exec ? 'regexp' :// some browsers (FF) consider regexps functions + obj.getHours ? 'date' : + obj.scrollBy ? 'window' : + obj.nodeName == '#document' ? 'document' : + obj.nodeName ? 'node' : + obj.item ? 'nodelist' : // Safari reports nodelists as functions + obj.callee ? 'arguments' : + obj.call || obj.constructor != Array && //an array would also fall on this hack + (obj+'').indexOf(f) != -1 ? f : //IE reports functions like alert, as objects + 'length' in obj ? 'array' : + type; + }, + separator:function(){ + return this.multiline ? this.HTML ? '
      ' : '\n' : this.HTML ? ' ' : ' '; + }, + indent:function( extra ){// extra can be a number, shortcut for increasing-calling-decreasing + if( !this.multiline ) + return ''; + var chr = this.indentChar; + if( this.HTML ) + chr = chr.replace(/\t/g,' ').replace(/ /g,' '); + return Array( this._depth_ + (extra||0) ).join(chr); + }, + up:function( a ){ + this._depth_ += a || 1; + }, + down:function( a ){ + this._depth_ -= a || 1; + }, + setParser:function( name, parser ){ + this.parsers[name] = parser; + }, + // The next 3 are exposed so you can use them + quote:quote, + literal:literal, + join:join, + // + _depth_: 1, + // This is the list of parsers, to modify them, use jsDump.setParser + parsers:{ + window: '[Window]', + document: '[Document]', + error:'[ERROR]', //when no parser is found, shouldn't happen + unknown: '[Unknown]', + 'null':'null', + undefined:'undefined', + 'function':function( fn ){ + var ret = 'function', + name = 'name' in fn ? fn.name : (reName.exec(fn)||[])[1];//functions never have name in IE + if( name ) + ret += ' ' + name; + ret += '('; + + ret = [ ret, this.parse( fn, 'functionArgs' ), '){'].join(''); + return join( ret, this.parse(fn,'functionCode'), '}' ); + }, + array: array, + nodelist: array, + arguments: array, + object:function( map ){ + var ret = [ ]; + this.up(); + for( var key in map ) + ret.push( this.parse(key,'key') + ': ' + this.parse(map[key]) ); + this.down(); + return join( '{', ret, '}' ); + }, + node:function( node ){ + var open = this.HTML ? '<' : '<', + close = this.HTML ? '>' : '>'; + + var tag = node.nodeName.toLowerCase(), + ret = open + tag; + + for( var a in this.DOMAttrs ){ + var val = node[this.DOMAttrs[a]]; + if( val ) + ret += ' ' + a + '=' + this.parse( val, 'attribute' ); + } + return ret + close + open + '/' + tag + close; + }, + functionArgs:function( fn ){//function calls it internally, it's the arguments part of the function + var l = fn.length; + if( !l ) return ''; + + var args = Array(l); + while( l-- ) + args[l] = String.fromCharCode(97+l);//97 is 'a' + return ' ' + args.join(', ') + ' '; + }, + key:quote, //object calls it internally, the key part of an item in a map + functionCode:'[code]', //function calls it internally, it's the content of the function + attribute:quote, //node calls it internally, it's an html attribute value + string:quote, + date:quote, + regexp:literal, //regex + number:literal, + 'boolean':literal + }, + DOMAttrs:{//attributes to dump from nodes, name=>realName + id:'id', + name:'name', + 'class':'className' + }, + HTML:false,//if true, entities are escaped ( <, >, \t, space and \n ) + indentChar:' ',//indentation unit + multiline:true //if true, items in a collection, are separated by a \n, else just a space. + }; + +})(); Index: modules/simpletest/qunit.php =================================================================== RCS file: modules/simpletest/qunit.php diff -N modules/simpletest/qunit.php --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ modules/simpletest/qunit.php 1 Jan 1970 00:00:00 -0000 @@ -0,0 +1,18 @@ + $value) { + $_SERVER[$key] = str_replace('modules/simpletest/qunit.php', 'index.php', $value); +} + +$_GET['q'] = 'simpletest/js/qunit'; + +require_once('./index.php'); Index: modules/simpletest/test.js =================================================================== RCS file: modules/simpletest/test.js diff -N modules/simpletest/test.js --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ modules/simpletest/test.js 1 Jan 1970 00:00:00 -0000 @@ -0,0 +1,60 @@ +// + +(function($) { + +/** + * Provide a Drupal-specific wrapper for the QUnit JavaScript test framework. + */ +Drupal.tests = Drupal.tests || {}; +Drupal.dependencies = Drupal.dependencies || []; + +Drupal.behaviors.runTests = { + attach: function(context, settings) { + var index; + var loaded = 0; + // Note: never register a dependency on drupal.js, it is loaded + // automatically anyway and will send this into an infinite loop. + for (index in Drupal.dependencies) { + $.getScript(Drupal.dependencies[index](settings), function() { + loaded++; + if (loaded == Drupal.dependencies.length) { + console.log('All loaded'); + } + }); + } + + if (Drupal.settings.activeTest) { + var index = Drupal.settings.activeTest; + var testCase = Drupal.tests[index]; + var info = testCase.getInfo(); + module(info.group, testCase); + test(info.name + '' + index + '' + '' + info.description + '', testCase.test); + } + else { + for (index in Drupal.tests) { + var testCase = Drupal.tests[index]; + var info = testCase.getInfo(); + module(info.group, testCase); + test(info.name + '' + index + '' + '' + info.description + '', testCase.test); + } + } + + // After we've finished running all of them, they should be on the page, so + // send a post request back to Drupal. + setTimeout(function() { + $('#tests').find('> li').each(function() { + data = {}; + data.index = $(this).find('> strong > span > span').get(0).innerHTML; + index = 0; + $(this).find('> ol > li').each(function() { + data['assertions[' + index + '][status]'] = $(this).hasClass('pass'); + data['assertions[' + index + '][message]'] = this.innerHTML; + index++; + }); + $.post(Drupal.settings.basePath + '?q=simpletest/js/record', data); + }); + }, 100); + } +}; + +})(jQuery); Index: modules/simpletest/simpletest.qunit.tpl.php =================================================================== RCS file: modules/simpletest/simpletest.qunit.tpl.php diff -N modules/simpletest/simpletest.qunit.tpl.php --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ modules/simpletest/simpletest.qunit.tpl.php 1 Jan 1970 00:00:00 -0000 @@ -0,0 +1,23 @@ + + + + QUnit test runner + + + + +

      Drupal JavaScript Test Suite

      + +

      +
        + + + +