diff --git a/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php b/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php index 1b746e6..61a8bd3 100644 --- a/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php +++ b/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php @@ -1344,4 +1344,42 @@ $target_storage->write($name, $source_storage->read($name)); } } + + /** + * Performs a CSS selection based search on the current page contents. + * + * Utilizes \PHPUnit_Util_XML::cssSelect to perform a CSS based search of the + * contents of the internal browser. The search is relative to the root + * element (HTML tag normally) of the page. + * + * @param string $selector + * CSS selector to use in the search. + * @param string $contains + * (optional) Filter matching nodes to those containing the given content. + * Note that due to limitations with \PHPUnit_Util_XML::cssSelect this does + * not work when you pass a nested selector such as 'li h2'. + * @param mixed $content + * (optional) Context to parse for selector. Defaults to $this->content. + * + * @return array + * Array of matching \DOMElement nodes. + * + * @see \PHPUnit_Util_XML::cssSelect + */ + protected function cssSelect($selector, $contains = TRUE, $content = FALSE) { + if (!$content && isset($this->content)) { + $content = $this->content; + } + if (is_array($content)) { + $content = array_reduce($content, function ($value, $item) { + $value .= $item->ownerDocument->saveHtml($item); + return $value; + }, ''); + } + if ($nodes = \PHPUnit_Util_XML::cssSelect($selector, $contains, $content, TRUE)) { + return $nodes; + } + return array(); + } + } diff --git a/core/modules/simpletest/lib/Drupal/simpletest/Tests/SimpleTestUnitTest.php b/core/modules/simpletest/lib/Drupal/simpletest/Tests/SimpleTestUnitTest.php new file mode 100644 index 0000000..8af5204 --- /dev/null +++ b/core/modules/simpletest/lib/Drupal/simpletest/Tests/SimpleTestUnitTest.php @@ -0,0 +1,73 @@ + 'SimpleTest functionality (Unit test)', + 'description' => "Tests some methods of Simpletest as unit test.", + 'group' => 'SimpleTest' + ); + } + + /** + * Tests the cssSelect method. + * + * @see \PHPUnit_Util_XML::cssSelect() + */ + public function testCssSelect() { + $content = '
empty 1
+
+

+ My Subchild + My Child +

+ + Test Id Text +
+
classy
'; + + $root = $this->cssSelect('div', TRUE, $content); + $this->assertEqual(count($root), 3, '3 divs found'); + + $root = $this->cssSelect('#test_id', TRUE, $content); + $this->assertEqual(count($root), 1, 'ID selector works'); + + $child = $this->cssSelect('p', 'My Subchild My Child', $content); + $this->assertEqual(count($child), 1, 'Text content found.'); + + $child = $this->cssSelect('#test_subchild_id', 'My Subchild', $content); + $this->assertEqual(count($child), 1, 'Text match found.'); + + $child = $this->cssSelect('#test_subchild_id', 'Wrong text', $content); + $this->assertEqual(count($child), 0, 'Text match not found'); + + $child = $this->cssSelect('div p', TRUE, $content); + $this->assertEqual(count($child), 1, 'Nested selector'); + + $child = $this->cssSelect('div > span', TRUE, $content); + $this->assertEqual(count($child), 0, 'Nested immediate child selector.'); + + $child = $this->cssSelect('div span p', TRUE, $content); + $this->assertEqual(count($child), 0, 'Nested selector wrong order'); + + $child = $this->cssSelect('div.item', 'classy', $content); + $this->assertEqual(count($child), 1, 'Element with class'); + + $child = $this->cssSelect('span.item', 'classy', $content); + $this->assertEqual(count($child), 0, 'Element with class'); + } + +} diff --git a/core/modules/tour/lib/Drupal/tour/Tests/TourTest.php b/core/modules/tour/lib/Drupal/tour/Tests/TourTest.php index db69e18..cb0a44a 100644 --- a/core/modules/tour/lib/Drupal/tour/Tests/TourTest.php +++ b/core/modules/tour/lib/Drupal/tour/Tests/TourTest.php @@ -8,12 +8,12 @@ namespace Drupal\tour\Tests; use Drupal\Core\Language\Language; -use Drupal\simpletest\WebTestBase; +use Drupal\tour\Tests\TourTestBase; /** * Tests tour functionality. */ -class TourTest extends WebTestBase { +class TourTest extends TourTestBase { /** * Modules to enable. @@ -40,33 +40,27 @@ class TourTest extends WebTestBase { * Test tour functionality. */ public function testTourFunctionality() { + $this->loadTours('tour_test'); // Navigate to tour-test-1 and verify the tour_test_1 tip is found with appropriate classes. $this->drupalGet('tour-test-1'); - $elements = $this->xpath('//li[@data-id=:data_id and @class=:classes and ./h2[contains(., :text)]]', array( - ':classes' => 'tip-module-tour-test tip-type-text tip-tour-test-1 even last', - ':data_id' => 'tour-test-1', - ':text' => 'The first tip', - )); + $tour = $this->getTour('tour-test'); + $this->hasTipsOnPage($tour); + + $elements = $this->cssSelect('h2', 'The first tip', $this->cssSelect("li.tip-module-tour-test.tip-type-text.tip-tour-test-1.even.last[data-id=tour-test-1]")); $this->assertEqual(count($elements), 1, 'Found English variant of tip 1.'); - $elements = $this->xpath('//li[@data-id=:data_id and @class=:classes and ./p//a[@href=:href and contains(., :text)]]', array( - ':classes' => 'tip-module-tour-test tip-type-text tip-tour-test-1 even last', - ':data_id' => 'tour-test-1', - ':href' => url('', array('absolute' => TRUE)), - ':text' => 'Drupal', - )); + // Check for the token replacements done in [site:name] + $context = $this->cssSelect('li.tip-type-text[data-id=tour-test-1'); + $context = $this->cssSelect('p', TRUE, $context); + $url = url('', array('absolute' => TRUE)); + + $elements = $this->cssSelect('a[href=' . $url . ']', 'Drupal', $context); $this->assertEqual(count($elements), 1, 'Found Token replacement.'); - $elements = $this->xpath('//li[@data-id=:data_id and ./h2[contains(., :text)]]', array( - ':data_id' => 'tour-test-2', - ':text' => 'The quick brown fox', - )); + $elements = $this->cssSelect('h2', 'The quick brown fox', $this->cssSelect("li[data-id=tour-test-2]")); $this->assertNotEqual(count($elements), 1, 'Did not find English variant of tip 2.'); - $elements = $this->xpath('//li[@data-id=:data_id and ./h2[contains(., :text)]]', array( - ':data_id' => 'tour-test-1', - ':text' => 'La pioggia cade in spagna', - )); + $elements = $this->cssSelect('h2', 'La pioggia cade in spagna', $this->cssSelect("li[data-id=tour-test-1]")); $this->assertNotEqual(count($elements), 1, 'Did not find Italian variant of tip 1.'); // Ensure that plugin's work. @@ -74,33 +68,27 @@ class TourTest extends WebTestBase { // Navigate to tour-test-2/subpath and verify the tour_test_2 tip is found. $this->drupalGet('tour-test-2/subpath'); - $elements = $this->xpath('//li[@data-id=:data_id and ./h2[contains(., :text)]]', array( - ':data_id' => 'tour-test-2', - ':text' => 'The quick brown fox', - )); + $tour = $this->getTour('tour-test-2'); + $this->hasTipsOnPage($tour, array('tour-test-2')); + + + $elements = $this->cssSelect('h2', 'The quick brown fox', $this->cssSelect("li[data-id=tour-test-2]")); $this->assertEqual(count($elements), 1, 'Found English variant of tip 2.'); - $elements = $this->xpath('//li[@data-id=:data_id and ./h2[contains(., :text)]]', array( - ':data_id' => 'tour-test-1', - ':text' => 'The first tip', - )); + $elements = $this->cssSelect('h2', 'The first tip', $this->cssSelect("li[data-id=tour-test-1]")); $this->assertNotEqual(count($elements), 1, 'Did not find English variant of tip 1.'); // Enable Italian language and navigate to it/tour-test1 and verify italian // version of tip is found. language_save(new Language(array('langcode' => 'it'))); $this->drupalGet('it/tour-test-1'); + $tour = $this->getTour('tour-test'); + $this->hasTipsOnPage($tour); - $elements = $this->xpath('//li[@data-id=:data_id and ./h2[contains(., :text)]]', array( - ':data_id' => 'tour-test-1', - ':text' => 'La pioggia cade in spagna', - )); + $elements = $this->cssSelect('h2', 'La pioggia cade in spagna', $this->cssSelect("li[data-id=tour-test-1]")); $this->assertEqual(count($elements), 1, 'Found Italian variant of tip 1.'); - $elements = $this->xpath('//li[@data-id=:data_id and ./h2[contains(., :text)]]', array( - ':data_id' => 'tour-test-1', - ':text' => 'The first tip', - )); + $elements = $this->cssSelect('h2', 'The quick brown fox', $this->cssSelect("li[data-id=tour-test-2]")); $this->assertNotEqual(count($elements), 1, 'Did not find English variant of tip 1.'); language_save(new Language(array('langcode' => 'en'))); @@ -138,18 +126,12 @@ class TourTest extends WebTestBase { // Navigate to tour-test-1 and verify the new tip is found. $this->drupalGet('tour-test-1'); - $elements = $this->xpath('//li[@data-id=:data_id and ./h2[contains(., :text)]]', array( - ':data_id' => 'tour-code-test-1', - ':text' => 'The rain in spain', - )); + $elements = $this->cssSelect('h2', 'The rain in spain', $this->cssSelect("li[data-id=tour-code-test-1]")); $this->assertEqual(count($elements), 1, 'Found the required tip markup for tip 4'); // Verify that the weight sorting works by ensuring the lower weight item // (tip 4) has the 'End tour' button. - $elements = $this->xpath('//li[@data-id=:data_id and @data-text=:text]', array( - ':data_id' => 'tour-code-test-1', - ':text' => 'End tour', - )); + $elements = $this->cssSelect("li[data-id=tour-code-test-1][data-text*=End]"); $this->assertEqual(count($elements), 1, 'Found code tip was weighted last and had "End tour".'); // Test hook_tour_alter(). diff --git a/core/modules/tour/lib/Drupal/tour/Tests/TourTestBase.php b/core/modules/tour/lib/Drupal/tour/Tests/TourTestBase.php new file mode 100644 index 0000000..7dd7d7e --- /dev/null +++ b/core/modules/tour/lib/Drupal/tour/Tests/TourTestBase.php @@ -0,0 +1,173 @@ +tours = array(); + $tours = drupal_container()->get('config.storage')->listAll('tour.tour.'); + + foreach ($tours as $name) { + //$schema = config_typed()->getDefinition($name); + $tour = config_typed()->get($name)->getValue(); + $this->tours[$name] = $tour; + } + } + + /** + * Load tours defined by given module. + * + * It should be possible to have tours not defined by modules but + * added to active config. + * + * @param string $module + */ + function loadTours($module = NULL) { + $this->loadToursAll(); + $this->active_tours = array_filter($this->tours, function($item) use ($module) { + return empty($module) || $item['module'] == $module; + }); + $tours = join(', ', array_keys($this->active_tours)); + $this->assertTrue(count($this->active_tours), format_string('Found %tours.', array('%tours' => $tours), 'Tours')); + $this->verbose(print_r($this->active_tours, TRUE)); + } + + /** + * Returns one of the active tours. + * + * @param type $id + * @return type + */ + function getTour($id) { + $tours = array_filter($this->active_tours, function($item) use ($id) { + return $item['id'] == $id; + }); + $this->assertEqual(count($tours), 1, "Tour $id found"); + if (count($tours) == 1) { + return array_shift($tours); + } + } + + /** + * Tests for tour on current page. + * + * If no tour is found there is no tour for the page + * or the current user may not see it. Check your access settings. + * + * @return boolean + */ + function hasTourOnPage() { + $path = '//ol[@id="tour"]'; + $elements = $this->xpath($path); + $tour_found = count($elements) == 1; + $this->assertTrue($tour_found, "Tour found on page"); + return $tour_found; + } + + /** + * Check the current page for the given tour optional filtered by give tip IDs. + * + * Both the tip as the tip target are checked for. + * + * TODO: + * - order tips by weight? This will help a Tour tester. + * + * @param array $tour + * @param array $tip_ids + */ + function hasTipsOnPage(array $tour, array $tip_ids = array()) { + $tour_id = $tour['id']; + if (empty($tips_ids)) { + $tips_ids = array_keys($tour['tips']); + } + foreach ($tips_ids as $id) { + $tip = $tour['tips'][$id]; + if (isset($tip['attributes'])) { + $attributes = $tip['attributes']; + $selector = array(); + if (isset($attributes['data-id'])) { + $selector[':data-id'] = $tip['attributes']['data-id']; + $this->tipOnPage($tip['id'], $selector); + $this->tipTargetOnPage($tip['id'], $selector); + } + if (isset($attributes['data-class'])) { + $selector[':data-class'] = $tip['attributes']['data-class']; + $this->tipOnPage($tip['id'], $selector); + $this->tipTargetOnPage($tip['id'], $selector); + } + if (empty($selector)) { + // Modal tip + $this->assertTrue(empty($selector), "Modal tip"); + } + elseif (count($selector) > 1) { + $this->assertTrue(count($selector) > 1, 'Do not use both data-id and data-class.'); + } + } + } + } + + function tipOnPage($tip_id, $selector) { + //TODO: + // Use the #tour selector: fails on '//ol[@id=tour]/li[' + $path = '//li['; + if (isset($selector[':data-id'])) { + $path .= '@data-id=:data-id and '; + } + if (isset($selector[':data-class'])) { + // No need to parse path as we select on data-class not on real class. + $path .= '@data-class=:data-class and '; + } + $path .= ' ./h2]'; + $elements = $this->xpath($path, $selector); + $this->assertEqual(count($elements), 1, format_string("Found tip for selection '$path' on '$tip_id' ", $selector)); + } + + /** + * Test for the tip target. + * + * When the target is not found this means either to page content changed + * or the tour selector is wrong. + * + * @param type $tip_id + * @param type $selector + */ + function tipTargetOnPage($tip_id, $selector) { + if (isset($selector[':data-id'])) { + $path = '//*[' . '@id=:data-id' . ']'; + $elements = $this->xpath($path, $selector); + } + else if (isset($selector[':data-class'])) { + $path = $selector[':data-class']; + // TODO: somehow it seems first item is ALWAYS a class!?! + $class = '.' . $path; + // Normalize spaces + $class = preg_replace('/\s\s+/', ' ', $class); + $css_paths = explode(' ', $class); + $context = FALSE; + while (!empty($css_paths)) { + $css_path = array_shift($css_paths); + $context = $this->cssSelect($css_path, TRUE, $context); + } + $elements = $context; + } + $this->assertEqual(count($elements), 1, format_string("Found target for selection '$path' on '$tip_id' ", $selector)); + } + +} diff --git a/core/modules/tour/tests/tour_test/config/tour.tour.tour-test-2.yml b/core/modules/tour/tests/tour_test/config/tour.tour.tour-test-2.yml index 62e5ae8..019d425 100644 --- a/core/modules/tour/tests/tour_test/config/tour.tour.tour-test-2.yml +++ b/core/modules/tour/tests/tour_test/config/tour.tour.tour-test-2.yml @@ -12,4 +12,4 @@ tips: body: Per lo piĆ¹ in pianura. weight: "2" attributes: - data-id: tour-test-2 + data-id: tour-test-target-2 diff --git a/core/modules/tour/tests/tour_test/config/tour.tour.tour-test.yml b/core/modules/tour/tests/tour_test/config/tour.tour.tour-test.yml index 9d389ed..ff92728 100644 --- a/core/modules/tour/tests/tour_test/config/tour.tour.tour-test.yml +++ b/core/modules/tour/tests/tour_test/config/tour.tour.tour-test.yml @@ -12,7 +12,7 @@ tips: body: Is [site:name] always the best dressed? weight: "1" attributes: - data-id: tour-test-1 + data-id: tour-test-target-1 tour-test-3: id: tour-test-3 plugin: image @@ -20,4 +20,4 @@ tips: url: http://local/image.png weight: "1" attributes: - data-id: tour-test-3 + data-id: tour-test-target-3 diff --git a/core/modules/tour/tests/tour_test/lib/Drupal/tour_test/Controller/TourTestController.php b/core/modules/tour/tests/tour_test/lib/Drupal/tour_test/Controller/TourTestController.php index 73b7cde..910ab27 100644 --- a/core/modules/tour/tests/tour_test/lib/Drupal/tour_test/Controller/TourTestController.php +++ b/core/modules/tour/tests/tour_test/lib/Drupal/tour_test/Controller/TourTestController.php @@ -29,14 +29,21 @@ class TourTestController implements ControllerInterface { 'tip-1' => array( '#type' => 'container', '#attributes' => array( - 'id' => 'tour-test-1', + 'id' => 'tour-test-target-1', ), '#children' => t('Where does the rain in Spain fail?'), ), + 'burp' => array( + '#type' => 'container', + '#attributes' => array( + 'id' => 'tour-test-target-3', + ), + '#children' => t('Target for the image plugin'), + ), 'tip-4' => array( '#type' => 'container', '#attributes' => array( - 'id' => 'tour-test-4', + 'id' => 'tour-test-target-4', ), '#children' => t('Tip created later?'), ), @@ -50,7 +57,7 @@ class TourTestController implements ControllerInterface { return array( '#type' => 'container', '#attributes' => array( - 'id' => 'tour-test-2', + 'id' => 'tour-test-target-2', ), '#children' => t('Pangram example'), ); diff --git a/core/modules/views_ui/config/tour.tour.views-ui.yml b/core/modules/views_ui/config/tour.tour.views-ui.yml index 6f2ca9d..981effc 100644 --- a/core/modules/views_ui/config/tour.tour.views-ui.yml +++ b/core/modules/views_ui/config/tour.tour.views-ui.yml @@ -4,6 +4,7 @@ label: Views ui langcode: en paths: - admin/structure/views/view/*/edit + - admin/structure/views/view/* tips: views-ui-active-display: id: views-ui-active-display diff --git a/core/modules/views_ui/lib/Drupal/views_ui/Tests/TourTest.php b/core/modules/views_ui/lib/Drupal/views_ui/Tests/TourTest.php new file mode 100644 index 0000000..9c5c739 --- /dev/null +++ b/core/modules/views_ui/lib/Drupal/views_ui/Tests/TourTest.php @@ -0,0 +1,59 @@ + 'Views Tours', + 'description' => 'Tests the tours provided by Views UI.', + 'group' => 'Tour', + ); + } + + public function setUp() { + parent::setUp(); + + // Add an admin user will full rights; + $this->admin = $this->drupalCreateUser(array('access tour', 'administer views')); + } + + /** + * Tests that analyze works in general. + */ + function testTourViewEdit() { + $this->drupalLogin($this->admin); + + $this->loadTours('views_ui'); + + $this->drupalGet('admin/structure/views/view/frontpage/edit'); + $this->hasTourOnPage(); + $tour = $this->getTour('views-ui'); + $this->hasTipsOnPage($tour); + + $this->drupalGet('admin/structure/views/view/frontpage'); + $tour = $this->getTour('views-ui'); + $this->hasTipsOnPage($tour); + + } + +}