Commit 5fb617d7 authored by Dries's avatar Dries

Issue #1938926 by sun, Cottser, joelpittet, pplantinga: Convert simpletest...

Issue #1938926 by sun, Cottser, joelpittet, pplantinga: Convert simpletest theme tables to table #type.
parent 03ddf8c2
......@@ -1921,18 +1921,7 @@ function drupal_html_id($id) {
}
$seen_ids = &drupal_static(__FUNCTION__, $seen_ids_init);
$id = strtr(drupal_strtolower($id), array(' ' => '-', '_' => '-', '[' => '-', ']' => ''));
// As defined in http://www.w3.org/TR/html4/types.html#type-name, HTML IDs can
// only contain letters, digits ([0-9]), hyphens ("-"), underscores ("_"),
// colons (":"), and periods ("."). We strip out any character not in that
// list. Note that the CSS spec doesn't allow colons or periods in identifiers
// (http://www.w3.org/TR/CSS21/syndata.html#characters), so we strip those two
// characters as well.
$id = preg_replace('/[^A-Za-z0-9\-_]/', '', $id);
// Removing multiple consecutive hyphens.
$id = preg_replace('/\-+/', '-', $id);
$id = drupal_clean_id_identifier($id);
// Ensure IDs are unique by appending a counter after the first occurrence.
// The counter needs to be appended with a delimiter that does not exist in
// the base ID. Requiring a unique delimiter helps ensure that we really do
......@@ -1948,6 +1937,36 @@ function drupal_html_id($id) {
return $id;
}
/**
* Prepares a string for use as a valid HTML ID.
*
* Only use this function when you want to intentionally skip the uniqueness
* guarantee of drupal_html_id().
*
* @param string $id
* The ID to clean.
*
* @return string
* The cleaned ID.
*
* @see drupal_html_id()
*/
function drupal_clean_id_identifier($id) {
$id = strtr(drupal_strtolower($id), array(' ' => '-', '_' => '-', '[' => '-', ']' => ''));
// As defined in http://www.w3.org/TR/html4/types.html#type-name, HTML IDs can
// only contain letters, digits ([0-9]), hyphens ("-"), underscores ("_"),
// colons (":"), and periods ("."). We strip out any character not in that
// list. Note that the CSS spec doesn't allow colons or periods in identifiers
// (http://www.w3.org/TR/CSS21/syndata.html#characters), so we strip those two
// characters as well.
$id = preg_replace('/[^A-Za-z0-9\-_]/', '', $id);
// Removing multiple consecutive hyphens.
$id = preg_replace('/\-+/', '-', $id);
return $id;
}
/**
* Adds a JavaScript file, setting, or inline code to the page.
*
......
......@@ -1785,52 +1785,81 @@ function form_process_table($element, &$form_state) {
// tableselect element behaves as if it had been of #type checkboxes or
// radios.
foreach (element_children($element) as $key) {
$row = &$element[$key];
// Prepare the element #parents for the tableselect form element.
// Their values have to be located in child keys (#tree is ignored), since
// form_validate_table() has to be able to validate whether input (for the
// parent #type 'table' element) has been submitted.
$element_parents = array_merge($element['#parents'], array($key));
// Since the #parents of the tableselect form element will equal the
// #parents of the row element, prevent FormBuilder from auto-generating
// an #id for the row element, since drupal_html_id() would automatically
// append a suffix to the tableselect form element's #id otherwise.
$row['#id'] = drupal_html_id('edit-' . implode('-', $element_parents) . '-row');
// Do not overwrite manually created children.
if (!isset($element[$key]['select'])) {
if (!isset($row['select'])) {
// Determine option label; either an assumed 'title' column, or the
// first available column containing a #title or #markup.
// @todo Consider to add an optional $element[$key]['#title_key']
// defaulting to 'title'?
$title = '';
if (!empty($element[$key]['title']['#title'])) {
$title = $element[$key]['title']['#title'];
unset($label_element);
$title = NULL;
if (isset($row['title']['#type']) && $row['title']['#type'] == 'label') {
$label_element = &$row['title'];
}
else {
foreach (element_children($element[$key]) as $column) {
if (isset($element[$key][$column]['#title'])) {
$title = $element[$key][$column]['#title'];
break;
}
if (isset($element[$key][$column]['#markup'])) {
$title = $element[$key][$column]['#markup'];
break;
if (!empty($row['title']['#title'])) {
$title = $row['title']['#title'];
}
else {
foreach (element_children($row) as $column) {
if (isset($row[$column]['#title'])) {
$title = $row[$column]['#title'];
break;
}
if (isset($row[$column]['#markup'])) {
$title = $row[$column]['#markup'];
break;
}
}
}
}
if ($title !== '') {
$title = t('Update !title', array('!title' => $title));
if (isset($title) && $title !== '') {
$title = t('Update !title', array('!title' => $title));
}
}
// Prepend the select column to existing columns.
$element[$key] = array('select' => array()) + $element[$key];
$element[$key]['select'] += array(
$row = array('select' => array()) + $row;
$row['select'] += array(
'#type' => $element['#multiple'] ? 'checkbox' : 'radio',
'#title' => $title,
'#title_display' => 'invisible',
'#id' => drupal_html_id('edit-' . implode('-', $element_parents)),
// @todo If rows happen to use numeric indexes instead of string keys,
// this results in a first row with $key === 0, which is always FALSE.
'#return_value' => $key,
'#attributes' => $element['#attributes'],
'#wrapper_attributes' => array(
'class' => array('table-select'),
),
);
$element_parents = array_merge($element['#parents'], array($key));
if ($element['#multiple']) {
$element[$key]['select']['#default_value'] = isset($value[$key]) ? $key : NULL;
$element[$key]['select']['#parents'] = $element_parents;
$row['select']['#default_value'] = isset($value[$key]) ? $key : NULL;
$row['select']['#parents'] = $element_parents;
}
else {
$element[$key]['select']['#default_value'] = ($element['#default_value'] == $key ? $key : NULL);
$element[$key]['select']['#parents'] = $element['#parents'];
$element[$key]['select']['#id'] = drupal_html_id('edit-' . implode('-', $element_parents));
$row['select']['#default_value'] = ($element['#default_value'] == $key ? $key : NULL);
$row['select']['#parents'] = $element['#parents'];
}
if ($label_element) {
$label_element['#id'] = $row['select']['#id'] . '--label';
$label_element['#for'] = $row['select']['#id'];
$row['select']['#attributes']['aria-labelledby'] = $label_element['#id'];
$row['select']['#title_display'] = 'none';
}
else {
$row['select']['#title'] = $title;
$row['select']['#title_display'] = 'invisible';
}
}
}
......@@ -2958,7 +2987,17 @@ function theme_form_element_label($variables) {
$attributes['class'] = 'visually-hidden';
}
if (!empty($element['#id'])) {
// A #for property of a dedicated #type 'label' element as precedence.
if (!empty($element['#for'])) {
$attributes['for'] = $element['#for'];
// A custom #id allows the referenced form input element to refer back to
// the label element; e.g., in the 'aria-labelledby' attribute.
if (!empty($element['#id'])) {
$attributes['id'] = $element['#id'];
}
}
// Otherwise, point to the #id of the form input element.
elseif (!empty($element['#id'])) {
$attributes['for'] = $element['#id'];
}
......
......@@ -3,7 +3,7 @@
#simpletest-form-table th.select-all {
width: 1em;
}
th.simpletest_test {
th.simpletest-test-label {
width: 16em;
}
......
......@@ -7,6 +7,8 @@
namespace Drupal\simpletest\Form;
use Drupal\Component\Utility\SortArray;
use Drupal\Component\Utility\String;
use Drupal\Core\Form\FormBase;
/**
......@@ -46,14 +48,45 @@ public function buildForm(array $form, array &$form_state) {
);
$form['tests'] = array(
'#type' => 'details',
'#title' => $this->t('Tests'),
'#open' => TRUE,
'#description' => $this->t('Select the test(s) or test group(s) you would like to run, and click <em>Run tests</em>.'),
'#type' => 'table',
'#id' => 'simpletest-form-table',
'#tableselect' => TRUE,
'#header' => array(
array('data' => $this->t('Test'), 'class' => array('simpletest-test-label')),
array('data' => $this->t('Description'), 'class' => array('simpletest-test-description')),
),
'#empty' => $this->t('No tests to display.'),
'#attached' => array(
'library' => array(
array('simpletest', 'drupal.simpletest'),
),
),
);
$form['tests']['table'] = array(
'#theme' => 'simpletest_test_table',
// Define the images used to expand/collapse the test groups.
$image_collapsed = array(
'#theme' => 'image',
'#uri' => 'core/misc/menu-collapsed.png',
'#width' => '7',
'#height' => '7',
'#alt' => $this->t('Expand'),
'#title' => $this->t('Expand'),
'#suffix' => '<a href="#" class="simpletest-collapse">(' . $this->t('Expand') . ')</a>',
);
$image_extended = array(
'#theme' => 'image',
'#uri' => 'core/misc/menu-expanded.png',
'#width' => '7',
'#height' => '7',
'#alt' => $this->t('Collapse'),
'#title' => $this->t('Collapse'),
'#suffix' => '<a href="#" class="simpletest-collapse">(' . $this->t('Collapse') . ')</a>',
);
$js = array(
'images' => array(
drupal_render($image_collapsed),
drupal_render($image_extended),
),
);
// Generate the list of tests arranged by group.
......@@ -62,24 +95,103 @@ public function buildForm(array $form, array &$form_state) {
$form_state['storage']['PHPUnit'] = $groups['PHPUnit'];
foreach ($groups as $group => $tests) {
$form['tests'][$group] = array(
'#attributes' => array('class' => array('simpletest-group')),
);
// Make the class name safe for output on the page by replacing all
// non-word/decimal characters with a dash (-).
$group_class = 'module-' . strtolower(trim(preg_replace("/[^\w\d]/", "-", $group)));
// Override tableselect column with custom selector for this group.
// This group-select-all checkbox is injected via JavaScript.
$form['tests'][$group]['select'] = array(
'#wrapper_attributes' => array(
'id' => $group_class,
'class' => array('simpletest-select-all'),
),
);
$form['tests'][$group]['title'] = array(
// Expand/collapse image.
'#prefix' => '<div class="simpletest-image" id="simpletest-test-group-' . $group_class . '"></div>',
'#markup' => '<label for="' . $group_class . '-select-all" class="simpletest-group-label">' . $group . '</label>',
'#wrapper_attributes' => array(
'class' => array('simpletest-group-label'),
),
);
$form['tests'][$group]['description'] = array(
'#markup' => '&nbsp;',
'#wrapper_attributes' => array(
'class' => array('simpletest-group-description'),
),
);
// Add individual tests to group.
$current_js = array(
'testClass' => $group_class . '-test',
'testNames' => array(),
// imageDirection maps to the 'images' index in the $js array.
'imageDirection' => 0,
'clickActive' => FALSE,
);
// Sort test classes within group alphabetically by name/label.
uasort($tests, function ($a, $b) {
return SortArray::sortByKeyString($a, $b, 'name');
});
// Cycle through each test within the current group.
foreach ($tests as $class => $info) {
$form['tests']['table'][$group][$class] = array(
'#type' => 'checkbox',
$test_id = drupal_clean_id_identifier($class);
$test_checkbox_id = 'edit-tests-' . $test_id;
$current_js['testNames'][] = $test_checkbox_id;
$form['tests'][$class] = array(
'#attributes' => array('class' => array($group_class . '-test', 'js-hide')),
);
$form['tests'][$class]['title'] = array(
'#type' => 'label',
'#title' => $info['name'],
'#description' => $info['description'],
'#wrapper_attributes' => array(
'class' => array('simpletest-test-label', 'table-filter-text-source'),
),
);
$form['tests'][$class]['description'] = array(
'#prefix' => '<div class="description">',
'#markup' => String::format('@description (@class)', array(
'@description' => $info['description'],
'@class' => $class,
)),
'#suffix' => '</div>',
'#wrapper_attributes' => array(
'class' => array('simpletest-test-description', 'table-filter-text-source'),
),
);
}
$js['simpletest-test-group-' . $group_class] = $current_js;
}
// Add JavaScript array of settings.
$form['tests']['#attached']['js'][] = array(
'type' => 'setting',
'data' => array('simpleTest' => $js),
);
// Action buttons.
$form['tests']['op'] = array(
$form['actions'] = array('#type' => 'actions');
$form['actions']['submit'] = array(
'#type' => 'submit',
'#value' => $this->t('Run tests'),
'#tableselect' => TRUE,
'#button_type' => 'primary',
);
$form['clean'] = array(
'#type' => 'fieldset',
'#title' => $this->t('Clean test environment'),
'#description' => $this->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.'),
'#weight' => 200,
);
$form['clean']['op'] = array(
'#type' => 'submit',
......@@ -94,21 +206,20 @@ public function buildForm(array $form, array &$form_state) {
* {@inheritdoc}
*/
public function submitForm(array &$form, array &$form_state) {
// Get list of tests.
$tests_list = array();
simpletest_classloader_register();
$phpunit_all = array_keys($form_state['storage']['PHPUnit']);
foreach ($form_state['values'] as $class_name => $value) {
$tests_list = array();
foreach ($form_state['values']['tests'] as $class_name => $value) {
// Since class_exists() will likely trigger an autoload lookup,
// we do the fast check first.
if ($value === 1 && class_exists($class_name)) {
if ($value === $class_name && class_exists($class_name)) {
$test_type = in_array($class_name, $phpunit_all) ? 'UnitTest' : 'WebTest';
$tests_list[$test_type][] = $class_name;
}
}
if (count($tests_list) > 0 ) {
if (!empty($tests_list)) {
$test_id = simpletest_run_tests($tests_list, 'drupal');
$form_state['redirect_route'] = array(
'route_name' => 'simpletest.result_form',
......@@ -117,9 +228,6 @@ public function submitForm(array &$form, array &$form_state) {
),
);
}
else {
drupal_set_message($this->t('No test(s) selected.'), 'error');
}
}
}
......@@ -89,7 +89,7 @@ function testMethod() {
if (!$this->isInChildSite()) {
// Verify that a broken setUp() method is caught.
file_put_contents($this->sharedTriggerFile, 'setup');
$edit['Drupal\simpletest\Tests\BrokenSetUpTest'] = TRUE;
$edit['tests[Drupal\simpletest\Tests\BrokenSetUpTest]'] = TRUE;
$this->drupalPostForm('admin/config/development/testing', $edit, t('Run tests'));
$this->assertRaw('Broken setup');
$this->assertNoRaw('The setUp() method has run.');
......@@ -100,7 +100,7 @@ function testMethod() {
// Verify that a broken tearDown() method is caught.
file_put_contents($this->sharedTriggerFile, 'teardown');
$edit['Drupal\simpletest\Tests\BrokenSetUpTest'] = TRUE;
$edit['tests[Drupal\simpletest\Tests\BrokenSetUpTest]'] = TRUE;
$this->drupalPostForm('admin/config/development/testing', $edit, t('Run tests'));
$this->assertNoRaw('Broken setup');
$this->assertRaw('The setUp() method has run.');
......@@ -111,7 +111,7 @@ function testMethod() {
// Verify that a broken test method is caught.
file_put_contents($this->sharedTriggerFile, 'test');
$edit['Drupal\simpletest\Tests\BrokenSetUpTest'] = TRUE;
$edit['tests[Drupal\simpletest\Tests\BrokenSetUpTest]'] = TRUE;
$this->drupalPostForm('admin/config/development/testing', $edit, t('Run tests'));
$this->assertNoRaw('Broken setup');
$this->assertRaw('The setUp() method has run.');
......
......@@ -57,7 +57,7 @@ function testInstallationProfileTests() {
$this->drupalGet('admin/config/development/testing');
$this->assertText('Installation profile module tests helper');
$edit = array(
'Drupal\drupal_system_listing_compatible_test\Tests\SystemListingCompatibleTest' => TRUE,
'tests[Drupal\drupal_system_listing_compatible_test\Tests\SystemListingCompatibleTest]' => TRUE,
);
$this->drupalPostForm(NULL, $edit, t('Run tests'));
$this->assertText('SystemListingCompatibleTest test executed.');
......
......@@ -55,7 +55,7 @@ protected function testCheckRequirements() {
// that the child tests did not run.
if (!$this->isInChildSite()) {
// Run this test from web interface.
$edit['Drupal\simpletest\Tests\MissingCheckedRequirementsTest'] = TRUE;
$edit['tests[Drupal\simpletest\Tests\MissingCheckedRequirementsTest]'] = TRUE;
$this->drupalPostForm('admin/config/development/testing', $edit, t('Run tests'));
$this->assertRaw('Test is not allowed to run.', 'Test check for requirements came up.');
$this->assertNoText('Test ran when it failed requirements check.', 'Test requirements stopped test from running.');
......
......@@ -162,7 +162,7 @@ function testWebTestRunner() {
$this->drupalGet('admin/config/development/testing');
$edit = array();
$edit['Drupal\simpletest\Tests\SimpleTestTest'] = TRUE;
$edit['tests[Drupal\simpletest\Tests\SimpleTestTest]'] = TRUE;
$this->drupalPostForm(NULL, $edit, t('Run tests'));
// Parse results and confirm that they are correct.
......
......@@ -26,6 +26,10 @@ function simpletest_help($path, $arg) {
$output .= '<p>' . t('After the tests run, a message will be displayed next to each test group indicating whether tests within it passed, failed, or had exceptions. A pass means that the test returned the expected results, while fail means that it did not. An exception normally indicates an error outside of the test, such as a PHP warning or notice. If there were failures or exceptions, the results will be expanded to show details, and the tests that had failures or exceptions will be indicated in red or pink rows. You can then use these results to refine your code and tests, until all tests pass.') . '</p></dd>';
$output .= '</dl>';
return $output;
case 'admin/config/development/testing':
$output = t('Select the test(s) or test group(s) you would like to run, and click <em>Run tests</em>.');
return $output;
}
}
......@@ -61,10 +65,6 @@ function simpletest_permission() {
*/
function simpletest_theme() {
return array(
'simpletest_test_table' => array(
'render element' => 'table',
'file' => 'simpletest.theme.inc',
),
'simpletest_result_summary' => array(
'render element' => 'form',
'file' => 'simpletest.theme.inc',
......
......@@ -5,154 +5,6 @@
* Page callbacks for simpletest module.
*/
/**
* Returns an HTML table for a test list generated by simpletest_test_form().
*
* @param $variables
* An associative array containing:
* - table: A render element representing the table.
*
* @ingroup themeable
*/
function theme_simpletest_test_table($variables) {
$table = $variables['table'];
drupal_add_library('simpletest', 'drupal.simpletest');
// Create header for test selection table.
$header = array(
array('class' => array('select-all')),
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.
$image_collapsed = array(
'#theme' => 'image',
'#uri' => 'core/misc/menu-collapsed.png',
'#width' => '7',
'#height' => '7',
'#alt' => t('Expand'),
'#title' => t('Expand'),
'#suffix' => '<a href="#" class="simpletest-collapse">(' . t('Expand') . ')</a>',
);
$image_extended = array(
'#theme' => 'image',
'#uri' => 'core/misc/menu-expanded.png',
'#width' => '7',
'#height' => '7',
'#alt' => t('Collapse'),
'#title' => t('Collapse'),
'#suffix' => ' <a href="#" class="simpletest-collapse">(' . t('Collapse') . ')</a>',
);
$js = array(
'images' => array(
drupal_render($image_collapsed),
drupal_render($image_extended),
),
);
// 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 = 'module-' . 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.
$open = !empty($element['#open']);
$image_index = $open ? 1 : 0;
// 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' => '<div class="simpletest-image" id="simpletest-test-group-' . $test_class . '"></div>' .
'<label for="' . $test_class . '-select-all" class="simpletest-group-label">' . $key . '</label>',
'class' => array('simpletest-group-label'),
);
$row[] = array(
'data' => '&nbsp;',
'class' => array('simpletest-group-description'),
);
$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, 'element_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'][] = $test['#id'];
// Store test title and description so that checkbox won't render them.
$title = $test['#title'];
$description = $test['#description'];
$test['#title_display'] = 'invisible';
unset($test['#description']);
// Test name is used to determine what tests to run.
$test['#name'] = $test_name;
$row[] = array(
'data' => drupal_render($test),
'class' => array('simpletest-test-select'),
);
$row[] = array(
'data' => '<label for="' . $test['#id'] . '">' . $title . '</label>',
'class' => array('simpletest-test-label', 'table-filter-text-source'),
);
$row[] = array(
'data' => '<div class="description">' . format_string('@description (@class)', array('@description' => $description, '@class' => $test_name)) . '</div>',
'class' => array('simpletest-test-description', 'table-filter-text-source'),
);
$rows[] = array('data' => $row, 'class' => array($test_class . '-test', ($open ? '' : 'js-hide')));
}
$js['simpletest-test-group-' . $test_class] = $current_js;
unset($table[$key]);
}
// Add js array of settings.
$attached = array();
$attached['#attached']['js'][] = array(
'data' => array('simpleTest' => $js),
'type' => 'setting',
);
drupal_render($attached);
if (empty($rows)) {
return '<strong>' . t('No tests to display.') . '</strong>';
}
else {
$simpletest_form_table = array(
'#theme' => 'table',
'#header' => $header,
'#rows' => $rows,
'#attributes' => array('id' => 'simpletest-form-table'),
);
return drupal_render($simpletest_form_table);
}
}
/**
* Returns HTML for the summary status of a simpletest result.
*
......
......@@ -520,6 +520,9 @@ function system_element_info() {
);
// Form structure.
$types['label'] = array(
'#theme' => 'form_element_label',
);
$types['item'] = array(
// Forms that show author fields to both anonymous and authenticated users
// need to dynamically switch between #type 'textfield' and #type 'item' to
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment