diff -r 07239de796bb -r e756a8c72c3d cms/drupal/modules/simpletest/tests/menu.test --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/cms/drupal/modules/simpletest/tests/menu.test Fri Sep 08 12:04:06 2017 +0200 @@ -0,0 +1,1740 @@ +drupalGet($goto); + } + // Compare paths with actual breadcrumb. + $parts = $this->getParts(); + $pass = TRUE; + foreach ($trail as $path => $title) { + $url = url($path); + $part = array_shift($parts); + $pass = ($pass && $part['href'] === $url && $part['text'] === check_plain($title)); + } + // No parts must be left, or an expected "Home" will always pass. + $pass = ($pass && empty($parts)); + + $this->assertTrue($pass, format_string('Breadcrumb %parts found on @path.', array( + '%parts' => implode(' » ', $trail), + '@path' => $this->getUrl(), + ))); + + // Additionally assert page title, if given. + if (isset($page_title)) { + $this->assertTitle(strtr('@title | Drupal', array('@title' => $page_title))); + } + + // Additionally assert active trail in a menu tree output, if given. + if ($tree) { + end($tree); + $active_link_path = key($tree); + $active_link_title = array_pop($tree); + $xpath = ''; + if ($tree) { + $i = 0; + foreach ($tree as $link_path => $link_title) { + $part_xpath = (!$i ? '//' : '/following-sibling::ul/descendant::'); + $part_xpath .= 'li[contains(@class, :class)]/a[contains(@href, :href) and contains(text(), :title)]'; + $part_args = array( + ':class' => 'active-trail', + ':href' => url($link_path), + ':title' => $link_title, + ); + $xpath .= $this->buildXPathQuery($part_xpath, $part_args); + $i++; + } + $elements = $this->xpath($xpath); + $this->assertTrue(!empty($elements), 'Active trail to current page was found in menu tree.'); + + // Append prefix for active link asserted below. + $xpath .= '/following-sibling::ul/descendant::'; + } + else { + $xpath .= '//'; + } + $xpath_last_active = ($last_active ? 'and contains(@class, :class-active)' : ''); + $xpath .= 'li[contains(@class, :class-trail)]/a[contains(@href, :href) ' . $xpath_last_active . 'and contains(text(), :title)]'; + $args = array( + ':class-trail' => 'active-trail', + ':class-active' => 'active', + ':href' => url($active_link_path), + ':title' => $active_link_title, + ); + $elements = $this->xpath($xpath, $args); + $this->assertTrue(!empty($elements), format_string('Active link %title was found in menu tree, including active trail links %tree.', array( + '%title' => $active_link_title, + '%tree' => implode(' » ', $tree), + ))); + } + } + + /** + * Returns the breadcrumb contents of the current page in the internal browser. + */ + protected function getParts() { + $parts = array(); + $elements = $this->xpath('//div[@class="breadcrumb"]/a'); + if (!empty($elements)) { + foreach ($elements as $element) { + $parts[] = array( + 'text' => (string) $element, + 'href' => (string) $element['href'], + 'title' => (string) $element['title'], + ); + } + } + return $parts; + } +} + +class MenuRouterTestCase extends DrupalWebTestCase { + public static function getInfo() { + return array( + 'name' => 'Menu router', + 'description' => 'Tests menu router and hook_menu() functionality.', + 'group' => 'Menu', + ); + } + + function setUp() { + // Enable dummy module that implements hook_menu. + parent::setUp('menu_test'); + // Make the tests below more robust by explicitly setting the default theme + // and administrative theme that they expect. + theme_enable(array('bartik')); + variable_set('theme_default', 'bartik'); + variable_set('admin_theme', 'seven'); + } + + /** + * Test title callback set to FALSE. + */ + function testTitleCallbackFalse() { + $this->drupalGet('node'); + $this->assertText('A title with @placeholder', 'Raw text found on the page'); + $this->assertNoText(t('A title with @placeholder', array('@placeholder' => 'some other text')), 'Text with placeholder substitutions not found.'); + } + + /** + * Tests page title of MENU_CALLBACKs. + */ + function testTitleMenuCallback() { + // Verify that the menu router item title is not visible. + $this->drupalGet(''); + $this->assertNoText(t('Menu Callback Title')); + // Verify that the menu router item title is output as page title. + $this->drupalGet('menu_callback_title'); + $this->assertText(t('Menu Callback Title')); + } + + /** + * Test the theme callback when it is set to use an administrative theme. + */ + function testThemeCallbackAdministrative() { + $this->drupalGet('menu-test/theme-callback/use-admin-theme'); + $this->assertText('Custom theme: seven. Actual theme: seven.', 'The administrative theme can be correctly set in a theme callback.'); + $this->assertRaw('seven/style.css', "The administrative theme's CSS appears on the page."); + } + + /** + * Test that the theme callback is properly inherited. + */ + function testThemeCallbackInheritance() { + $this->drupalGet('menu-test/theme-callback/use-admin-theme/inheritance'); + $this->assertText('Custom theme: seven. Actual theme: seven. Theme callback inheritance is being tested.', 'Theme callback inheritance correctly uses the administrative theme.'); + $this->assertRaw('seven/style.css', "The administrative theme's CSS appears on the page."); + } + + /** + * Test that 'page callback', 'file' and 'file path' keys are properly + * inherited from parent menu paths. + */ + function testFileInheritance() { + $this->drupalGet('admin/config/development/file-inheritance'); + $this->assertText('File inheritance test description', 'File inheritance works.'); + } + + /** + * Test path containing "exotic" characters. + */ + function testExoticPath() { + $path = "menu-test/ -._~!$'\"()*@[]?&+%#,;=:" . // "Special" ASCII characters. + "%23%25%26%2B%2F%3F" . // Characters that look like a percent-escaped string. + "éøïвβ中國書۞"; // Characters from various non-ASCII alphabets. + $this->drupalGet($path); + $this->assertRaw('This is menu_test_callback().'); + } + + /** + * Test the theme callback when the site is in maintenance mode. + */ + function testThemeCallbackMaintenanceMode() { + variable_set('maintenance_mode', TRUE); + + // For a regular user, the fact that the site is in maintenance mode means + // we expect the theme callback system to be bypassed entirely. + $this->drupalGet('menu-test/theme-callback/use-admin-theme'); + $this->assertRaw('bartik/css/style.css', "The maintenance theme's CSS appears on the page."); + + // An administrator, however, should continue to see the requested theme. + $admin_user = $this->drupalCreateUser(array('access site in maintenance mode')); + $this->drupalLogin($admin_user); + $this->drupalGet('menu-test/theme-callback/use-admin-theme'); + $this->assertText('Custom theme: seven. Actual theme: seven.', 'The theme callback system is correctly triggered for an administrator when the site is in maintenance mode.'); + $this->assertRaw('seven/style.css', "The administrative theme's CSS appears on the page."); + } + + /** + * Make sure the maintenance mode can be bypassed using hook_menu_site_status_alter(). + * + * @see hook_menu_site_status_alter(). + */ + function testMaintenanceModeLoginPaths() { + variable_set('maintenance_mode', TRUE); + + $offline_message = t('@site is currently under maintenance. We should be back shortly. Thank you for your patience.', array('@site' => variable_get('site_name', 'Drupal'))); + $this->drupalLogout(); + $this->drupalGet('node'); + $this->assertText($offline_message); + $this->drupalGet('menu_login_callback'); + $this->assertText('This is menu_login_callback().', t('Maintenance mode can be bypassed through hook_login_paths().')); + } + + /** + * Test that an authenticated user hitting 'user/login' gets redirected to + * 'user' and 'user/register' gets redirected to the user edit page. + */ + function testAuthUserUserLogin() { + $loggedInUser = $this->drupalCreateUser(array()); + $this->drupalLogin($loggedInUser); + + $this->drupalGet('user/login'); + // Check that we got to 'user'. + $this->assertTrue($this->url == url('user', array('absolute' => TRUE)), "Logged-in user redirected to q=user on accessing q=user/login"); + + // user/register should redirect to user/UID/edit. + $this->drupalGet('user/register'); + $this->assertTrue($this->url == url('user/' . $this->loggedInUser->uid . '/edit', array('absolute' => TRUE)), "Logged-in user redirected to q=user/UID/edit on accessing q=user/register"); + } + + /** + * Test the theme callback when it is set to use an optional theme. + */ + function testThemeCallbackOptionalTheme() { + // Request a theme that is not enabled. + $this->drupalGet('menu-test/theme-callback/use-stark-theme'); + $this->assertText('Custom theme: NONE. Actual theme: bartik.', 'The theme callback system falls back on the default theme when a theme that is not enabled is requested.'); + $this->assertRaw('bartik/css/style.css', "The default theme's CSS appears on the page."); + + // Now enable the theme and request it again. + theme_enable(array('stark')); + $this->drupalGet('menu-test/theme-callback/use-stark-theme'); + $this->assertText('Custom theme: stark. Actual theme: stark.', 'The theme callback system uses an optional theme once it has been enabled.'); + $this->assertRaw('stark/layout.css', "The optional theme's CSS appears on the page."); + } + + /** + * Test the theme callback when it is set to use a theme that does not exist. + */ + function testThemeCallbackFakeTheme() { + $this->drupalGet('menu-test/theme-callback/use-fake-theme'); + $this->assertText('Custom theme: NONE. Actual theme: bartik.', 'The theme callback system falls back on the default theme when a theme that does not exist is requested.'); + $this->assertRaw('bartik/css/style.css', "The default theme's CSS appears on the page."); + } + + /** + * Test the theme callback when no theme is requested. + */ + function testThemeCallbackNoThemeRequested() { + $this->drupalGet('menu-test/theme-callback/no-theme-requested'); + $this->assertText('Custom theme: NONE. Actual theme: bartik.', 'The theme callback system falls back on the default theme when no theme is requested.'); + $this->assertRaw('bartik/css/style.css', "The default theme's CSS appears on the page."); + } + + /** + * Test that hook_custom_theme() can control the theme of a page. + */ + function testHookCustomTheme() { + // Trigger hook_custom_theme() to dynamically request the Stark theme for + // the requested page. + variable_set('menu_test_hook_custom_theme_name', 'stark'); + theme_enable(array('stark')); + + // Visit a page that does not implement a theme callback. The above request + // should be honored. + $this->drupalGet('menu-test/no-theme-callback'); + $this->assertText('Custom theme: stark. Actual theme: stark.', 'The result of hook_custom_theme() is used as the theme for the current page.'); + $this->assertRaw('stark/layout.css', "The Stark theme's CSS appears on the page."); + } + + /** + * Test that the theme callback wins out over hook_custom_theme(). + */ + function testThemeCallbackHookCustomTheme() { + // Trigger hook_custom_theme() to dynamically request the Stark theme for + // the requested page. + variable_set('menu_test_hook_custom_theme_name', 'stark'); + theme_enable(array('stark')); + + // The menu "theme callback" should take precedence over a value set in + // hook_custom_theme(). + $this->drupalGet('menu-test/theme-callback/use-admin-theme'); + $this->assertText('Custom theme: seven. Actual theme: seven.', 'The result of hook_custom_theme() does not override what was set in a theme callback.'); + $this->assertRaw('seven/style.css', "The Seven theme's CSS appears on the page."); + } + + /** + * Tests for menu_link_maintain(). + */ + function testMenuLinkMaintain() { + $admin_user = $this->drupalCreateUser(array('administer site configuration')); + $this->drupalLogin($admin_user); + + // Create three menu items. + menu_link_maintain('menu_test', 'insert', 'menu_test_maintain/1', 'Menu link #1'); + menu_link_maintain('menu_test', 'insert', 'menu_test_maintain/1', 'Menu link #1-1'); + menu_link_maintain('menu_test', 'insert', 'menu_test_maintain/2', 'Menu link #2'); + + // Move second link to the main-menu, to test caching later on. + db_update('menu_links') + ->fields(array('menu_name' => 'main-menu')) + ->condition('link_title', 'Menu link #1-1') + ->condition('customized', 0) + ->condition('module', 'menu_test') + ->execute(); + menu_cache_clear('main-menu'); + + // Load front page. + $this->drupalGet('node'); + $this->assertLink(t('Menu link #1'), 0, 'Found menu link #1'); + $this->assertLink(t('Menu link #1-1'), 0, 'Found menu link #1-1'); + $this->assertLink(t('Menu link #2'), 0, 'Found menu link #2'); + + // Rename all links for the given path. + menu_link_maintain('menu_test', 'update', 'menu_test_maintain/1', 'Menu link updated'); + // Load a different page to be sure that we have up to date information. + $this->drupalGet('menu_test_maintain/1'); + $this->assertLink(t('Menu link updated'), 0, 'Found updated menu link'); + $this->assertNoLink(t('Menu link #1'), 0, 'Not found menu link #1'); + $this->assertNoLink(t('Menu link #1'), 0, 'Not found menu link #1-1'); + $this->assertLink(t('Menu link #2'), 0, 'Found menu link #2'); + + // Delete all links for the given path. + menu_link_maintain('menu_test', 'delete', 'menu_test_maintain/1', ''); + // Load a different page to be sure that we have up to date information. + $this->drupalGet('menu_test_maintain/2'); + $this->assertNoLink(t('Menu link updated'), 0, 'Not found deleted menu link'); + $this->assertNoLink(t('Menu link #1'), 0, 'Not found menu link #1'); + $this->assertNoLink(t('Menu link #1'), 0, 'Not found menu link #1-1'); + $this->assertLink(t('Menu link #2'), 0, 'Found menu link #2'); + } + + /** + * Test menu_get_names(). + */ + function testMenuGetNames() { + // Create three menu items. + for ($i = 0; $i < 3; $i++) { + $menu_link = array( + 'link_title' => 'Menu link #' . $i, + 'link_path' => 'menu_test/' . $i, + 'module' => 'menu_test', + 'menu_name' => 'menu_test_' . $i, + ); + menu_link_save($menu_link); + } + + drupal_static_reset('menu_get_names'); + + // Verify that the menu names are correctly reported by menu_get_names(). + $menu_names = menu_get_names(); + $this->pass(implode(' | ', $menu_names)); + for ($i = 0; $i < 3; $i++) { + $this->assertTrue(in_array('menu_test_' . $i, $menu_names), t('Expected menu name %expected is returned.', array('%expected' => 'menu_test_' . $i))); + } + } + + /** + * Tests for menu_name parameter for hook_menu(). + */ + function testMenuName() { + $admin_user = $this->drupalCreateUser(array('administer site configuration')); + $this->drupalLogin($admin_user); + + $sql = "SELECT menu_name FROM {menu_links} WHERE router_path = 'menu_name_test'"; + $name = db_query($sql)->fetchField(); + $this->assertEqual($name, 'original', 'Menu name is "original".'); + + // Change the menu_name parameter in menu_test.module, then force a menu + // rebuild. + menu_test_menu_name('changed'); + menu_rebuild(); + + $sql = "SELECT menu_name FROM {menu_links} WHERE router_path = 'menu_name_test'"; + $name = db_query($sql)->fetchField(); + $this->assertEqual($name, 'changed', 'Menu name was successfully changed after rebuild.'); + } + + /** + * Tests for menu hierarchy. + */ + function testMenuHierarchy() { + $parent_link = db_query('SELECT * FROM {menu_links} WHERE link_path = :link_path', array(':link_path' => 'menu-test/hierarchy/parent'))->fetchAssoc(); + $child_link = db_query('SELECT * FROM {menu_links} WHERE link_path = :link_path', array(':link_path' => 'menu-test/hierarchy/parent/child'))->fetchAssoc(); + $unattached_child_link = db_query('SELECT * FROM {menu_links} WHERE link_path = :link_path', array(':link_path' => 'menu-test/hierarchy/parent/child2/child'))->fetchAssoc(); + + $this->assertEqual($child_link['plid'], $parent_link['mlid'], 'The parent of a directly attached child is correct.'); + $this->assertEqual($unattached_child_link['plid'], $parent_link['mlid'], 'The parent of a non-directly attached child is correct.'); + } + + /** + * Tests menu link depth and parents of local tasks and menu callbacks. + */ + function testMenuHidden() { + // Verify links for one dynamic argument. + $links = db_select('menu_links', 'ml') + ->fields('ml') + ->condition('ml.router_path', 'menu-test/hidden/menu%', 'LIKE') + ->orderBy('ml.router_path') + ->execute() + ->fetchAllAssoc('router_path', PDO::FETCH_ASSOC); + + $parent = $links['menu-test/hidden/menu']; + $depth = $parent['depth'] + 1; + $plid = $parent['mlid']; + + $link = $links['menu-test/hidden/menu/list']; + $this->assertEqual($link['depth'], $depth, format_string('%path depth @link_depth is equal to @depth.', array('%path' => $link['router_path'], '@link_depth' => $link['depth'], '@depth' => $depth))); + $this->assertEqual($link['plid'], $plid, format_string('%path plid @link_plid is equal to @plid.', array('%path' => $link['router_path'], '@link_plid' => $link['plid'], '@plid' => $plid))); + + $link = $links['menu-test/hidden/menu/add']; + $this->assertEqual($link['depth'], $depth, format_string('%path depth @link_depth is equal to @depth.', array('%path' => $link['router_path'], '@link_depth' => $link['depth'], '@depth' => $depth))); + $this->assertEqual($link['plid'], $plid, format_string('%path plid @link_plid is equal to @plid.', array('%path' => $link['router_path'], '@link_plid' => $link['plid'], '@plid' => $plid))); + + $link = $links['menu-test/hidden/menu/settings']; + $this->assertEqual($link['depth'], $depth, format_string('%path depth @link_depth is equal to @depth.', array('%path' => $link['router_path'], '@link_depth' => $link['depth'], '@depth' => $depth))); + $this->assertEqual($link['plid'], $plid, format_string('%path plid @link_plid is equal to @plid.', array('%path' => $link['router_path'], '@link_plid' => $link['plid'], '@plid' => $plid))); + + $link = $links['menu-test/hidden/menu/manage/%']; + $this->assertEqual($link['depth'], $depth, format_string('%path depth @link_depth is equal to @depth.', array('%path' => $link['router_path'], '@link_depth' => $link['depth'], '@depth' => $depth))); + $this->assertEqual($link['plid'], $plid, format_string('%path plid @link_plid is equal to @plid.', array('%path' => $link['router_path'], '@link_plid' => $link['plid'], '@plid' => $plid))); + + $parent = $links['menu-test/hidden/menu/manage/%']; + $depth = $parent['depth'] + 1; + $plid = $parent['mlid']; + + $link = $links['menu-test/hidden/menu/manage/%/list']; + $this->assertEqual($link['depth'], $depth, format_string('%path depth @link_depth is equal to @depth.', array('%path' => $link['router_path'], '@link_depth' => $link['depth'], '@depth' => $depth))); + $this->assertEqual($link['plid'], $plid, format_string('%path plid @link_plid is equal to @plid.', array('%path' => $link['router_path'], '@link_plid' => $link['plid'], '@plid' => $plid))); + + $link = $links['menu-test/hidden/menu/manage/%/add']; + $this->assertEqual($link['depth'], $depth, format_string('%path depth @link_depth is equal to @depth.', array('%path' => $link['router_path'], '@link_depth' => $link['depth'], '@depth' => $depth))); + $this->assertEqual($link['plid'], $plid, format_string('%path plid @link_plid is equal to @plid.', array('%path' => $link['router_path'], '@link_plid' => $link['plid'], '@plid' => $plid))); + + $link = $links['menu-test/hidden/menu/manage/%/edit']; + $this->assertEqual($link['depth'], $depth, format_string('%path depth @link_depth is equal to @depth.', array('%path' => $link['router_path'], '@link_depth' => $link['depth'], '@depth' => $depth))); + $this->assertEqual($link['plid'], $plid, format_string('%path plid @link_plid is equal to @plid.', array('%path' => $link['router_path'], '@link_plid' => $link['plid'], '@plid' => $plid))); + + $link = $links['menu-test/hidden/menu/manage/%/delete']; + $this->assertEqual($link['depth'], $depth, format_string('%path depth @link_depth is equal to @depth.', array('%path' => $link['router_path'], '@link_depth' => $link['depth'], '@depth' => $depth))); + $this->assertEqual($link['plid'], $plid, format_string('%path plid @link_plid is equal to @plid.', array('%path' => $link['router_path'], '@link_plid' => $link['plid'], '@plid' => $plid))); + + // Verify links for two dynamic arguments. + $links = db_select('menu_links', 'ml') + ->fields('ml') + ->condition('ml.router_path', 'menu-test/hidden/block%', 'LIKE') + ->orderBy('ml.router_path') + ->execute() + ->fetchAllAssoc('router_path', PDO::FETCH_ASSOC); + + $parent = $links['menu-test/hidden/block']; + $depth = $parent['depth'] + 1; + $plid = $parent['mlid']; + + $link = $links['menu-test/hidden/block/list']; + $this->assertEqual($link['depth'], $depth, format_string('%path depth @link_depth is equal to @depth.', array('%path' => $link['router_path'], '@link_depth' => $link['depth'], '@depth' => $depth))); + $this->assertEqual($link['plid'], $plid, format_string('%path plid @link_plid is equal to @plid.', array('%path' => $link['router_path'], '@link_plid' => $link['plid'], '@plid' => $plid))); + + $link = $links['menu-test/hidden/block/add']; + $this->assertEqual($link['depth'], $depth, format_string('%path depth @link_depth is equal to @depth.', array('%path' => $link['router_path'], '@link_depth' => $link['depth'], '@depth' => $depth))); + $this->assertEqual($link['plid'], $plid, format_string('%path plid @link_plid is equal to @plid.', array('%path' => $link['router_path'], '@link_plid' => $link['plid'], '@plid' => $plid))); + + $link = $links['menu-test/hidden/block/manage/%/%']; + $this->assertEqual($link['depth'], $depth, format_string('%path depth @link_depth is equal to @depth.', array('%path' => $link['router_path'], '@link_depth' => $link['depth'], '@depth' => $depth))); + $this->assertEqual($link['plid'], $plid, format_string('%path plid @link_plid is equal to @plid.', array('%path' => $link['router_path'], '@link_plid' => $link['plid'], '@plid' => $plid))); + + $parent = $links['menu-test/hidden/block/manage/%/%']; + $depth = $parent['depth'] + 1; + $plid = $parent['mlid']; + + $link = $links['menu-test/hidden/block/manage/%/%/configure']; + $this->assertEqual($link['depth'], $depth, format_string('%path depth @link_depth is equal to @depth.', array('%path' => $link['router_path'], '@link_depth' => $link['depth'], '@depth' => $depth))); + $this->assertEqual($link['plid'], $plid, format_string('%path plid @link_plid is equal to @plid.', array('%path' => $link['router_path'], '@link_plid' => $link['plid'], '@plid' => $plid))); + + $link = $links['menu-test/hidden/block/manage/%/%/delete']; + $this->assertEqual($link['depth'], $depth, format_string('%path depth @link_depth is equal to @depth.', array('%path' => $link['router_path'], '@link_depth' => $link['depth'], '@depth' => $depth))); + $this->assertEqual($link['plid'], $plid, format_string('%path plid @link_plid is equal to @plid.', array('%path' => $link['router_path'], '@link_plid' => $link['plid'], '@plid' => $plid))); + } + + /** + * Test menu_get_item() with empty ancestors. + */ + function testMenuGetItemNoAncestors() { + variable_set('menu_masks', array()); + $this->drupalGet(''); + } + + /** + * Test menu_set_item(). + */ + function testMenuSetItem() { + $item = menu_get_item('node'); + + $this->assertEqual($item['path'], 'node', "Path from menu_get_item('node') is equal to 'node'", 'menu'); + + // Modify the path for the item then save it. + $item['path'] = 'node_test'; + $item['href'] = 'node_test'; + + menu_set_item('node', $item); + $compare_item = menu_get_item('node'); + $this->assertEqual($compare_item, $item, 'Modified menu item is equal to newly retrieved menu item.', 'menu'); + } + + /** + * Test menu maintenance hooks. + */ + function testMenuItemHooks() { + // Create an item. + menu_link_maintain('menu_test', 'insert', 'menu_test_maintain/4', 'Menu link #4'); + $this->assertEqual(menu_test_static_variable(), 'insert', 'hook_menu_link_insert() fired correctly'); + // Update the item. + menu_link_maintain('menu_test', 'update', 'menu_test_maintain/4', 'Menu link updated'); + $this->assertEqual(menu_test_static_variable(), 'update', 'hook_menu_link_update() fired correctly'); + // Delete the item. + menu_link_maintain('menu_test', 'delete', 'menu_test_maintain/4', ''); + $this->assertEqual(menu_test_static_variable(), 'delete', 'hook_menu_link_delete() fired correctly'); + } + + /** + * Test menu link 'options' storage and rendering. + */ + function testMenuLinkOptions() { + // Create a menu link with options. + $menu_link = array( + 'link_title' => 'Menu link options test', + 'link_path' => 'node', + 'module' => 'menu_test', + 'options' => array( + 'attributes' => array( + 'title' => 'Test title attribute', + ), + 'query' => array( + 'testparam' => 'testvalue', + ), + ), + ); + menu_link_save($menu_link); + + // Load front page. + $this->drupalGet('node'); + $this->assertRaw('title="Test title attribute"', 'Title attribute of a menu link renders.'); + $this->assertRaw('testparam=testvalue', 'Query parameter added to menu link.'); + } + + /** + * Tests the possible ways to set the title for menu items. + * Also tests that menu item titles work with string overrides. + */ + function testMenuItemTitlesCases() { + + // Build array with string overrides. + $test_data = array( + 1 => array('Example title - Case 1' => 'Alternative example title - Case 1'), + 2 => array('Example @sub1 - Case @op2' => 'Alternative example @sub1 - Case @op2'), + 3 => array('Example title' => 'Alternative example title'), + 4 => array('Example title' => 'Alternative example title'), + ); + + foreach ($test_data as $case_no => $override) { + $this->menuItemTitlesCasesHelper($case_no); + variable_set('locale_custom_strings_en', array('' => $override)); + $this->menuItemTitlesCasesHelper($case_no, TRUE); + variable_set('locale_custom_strings_en', array()); + } + } + + /** + * Get a URL and assert the title given a case number. If override is true, + * the title is asserted to begin with "Alternative". + */ + private function menuItemTitlesCasesHelper($case_no, $override = FALSE) { + $this->drupalGet('menu-title-test/case' . $case_no); + $this->assertResponse(200); + $asserted_title = $override ? 'Alternative example title - Case ' . $case_no : 'Example title - Case ' . $case_no; + $this->assertTitle($asserted_title . ' | Drupal', format_string('Menu title is: %title.', array('%title' => $asserted_title)), 'Menu'); + } + + /** + * Load the router for a given path. + */ + protected function menuLoadRouter($router_path) { + return db_query('SELECT * FROM {menu_router} WHERE path = :path', array(':path' => $router_path))->fetchAssoc(); + } + + /** + * Tests inheritance of 'load arguments'. + */ + function testMenuLoadArgumentsInheritance() { + $expected = array( + 'menu-test/arguments/%/%' => array( + 2 => array('menu_test_argument_load' => array(3)), + 3 => NULL, + ), + // Arguments are inherited to normal children. + 'menu-test/arguments/%/%/default' => array( + 2 => array('menu_test_argument_load' => array(3)), + 3 => NULL, + ), + // Arguments are inherited to tab children. + 'menu-test/arguments/%/%/task' => array( + 2 => array('menu_test_argument_load' => array(3)), + 3 => NULL, + ), + // Arguments are only inherited to the same loader functions. + 'menu-test/arguments/%/%/common-loader' => array( + 2 => array('menu_test_argument_load' => array(3)), + 3 => 'menu_test_other_argument_load', + ), + // Arguments are not inherited to children not using the same loader + // function. + 'menu-test/arguments/%/%/different-loaders-1' => array( + 2 => NULL, + 3 => 'menu_test_argument_load', + ), + 'menu-test/arguments/%/%/different-loaders-2' => array( + 2 => 'menu_test_other_argument_load', + 3 => NULL, + ), + 'menu-test/arguments/%/%/different-loaders-3' => array( + 2 => NULL, + 3 => NULL, + ), + // Explicit loader arguments should not be overriden by parent. + 'menu-test/arguments/%/%/explicit-arguments' => array( + 2 => array('menu_test_argument_load' => array()), + 3 => NULL, + ), + ); + + foreach ($expected as $router_path => $load_functions) { + $router_item = $this->menuLoadRouter($router_path); + $this->assertIdentical(unserialize($router_item['load_functions']), $load_functions, format_string('Expected load functions for router %router_path' , array('%router_path' => $router_path))); + } + } +} + +/** + * Tests for menu links. + */ +class MenuLinksUnitTestCase extends DrupalWebTestCase { + // Use the lightweight testing profile for this test. + protected $profile = 'testing'; + + public static function getInfo() { + return array( + 'name' => 'Menu links', + 'description' => 'Test handling of menu links hierarchies.', + 'group' => 'Menu', + ); + } + + /** + * Create a simple hierarchy of links. + */ + function createLinkHierarchy($module = 'menu_test') { + // First remove all the menu links. + db_truncate('menu_links')->execute(); + + // Then create a simple link hierarchy: + // - $parent + // - $child-1 + // - $child-1-1 + // - $child-1-2 + // - $child-2 + $base_options = array( + 'link_title' => 'Menu link test', + 'module' => $module, + 'menu_name' => 'menu_test', + ); + + $links['parent'] = $base_options + array( + 'link_path' => 'menu-test/parent', + ); + menu_link_save($links['parent']); + + $links['child-1'] = $base_options + array( + 'link_path' => 'menu-test/parent/child-1', + 'plid' => $links['parent']['mlid'], + ); + menu_link_save($links['child-1']); + + $links['child-1-1'] = $base_options + array( + 'link_path' => 'menu-test/parent/child-1/child-1-1', + 'plid' => $links['child-1']['mlid'], + ); + menu_link_save($links['child-1-1']); + + $links['child-1-2'] = $base_options + array( + 'link_path' => 'menu-test/parent/child-1/child-1-2', + 'plid' => $links['child-1']['mlid'], + ); + menu_link_save($links['child-1-2']); + + $links['child-2'] = $base_options + array( + 'link_path' => 'menu-test/parent/child-2', + 'plid' => $links['parent']['mlid'], + ); + menu_link_save($links['child-2']); + + return $links; + } + + /** + * Assert that at set of links is properly parented. + */ + function assertMenuLinkParents($links, $expected_hierarchy) { + foreach ($expected_hierarchy as $child => $parent) { + $mlid = $links[$child]['mlid']; + $plid = $parent ? $links[$parent]['mlid'] : 0; + + $menu_link = menu_link_load($mlid); + menu_link_save($menu_link); + $this->assertEqual($menu_link['plid'], $plid, format_string('Menu link %mlid has parent of %plid, expected %expected_plid.', array('%mlid' => $mlid, '%plid' => $menu_link['plid'], '%expected_plid' => $plid))); + } + } + + /** + * Test automatic reparenting of menu links. + */ + function testMenuLinkReparenting($module = 'menu_test') { + // Check the initial hierarchy. + $links = $this->createLinkHierarchy($module); + + $expected_hierarchy = array( + 'parent' => FALSE, + 'child-1' => 'parent', + 'child-1-1' => 'child-1', + 'child-1-2' => 'child-1', + 'child-2' => 'parent', + ); + $this->assertMenuLinkParents($links, $expected_hierarchy); + + // Start over, and move child-1 under child-2, and check that all the + // childs of child-1 have been moved too. + $links = $this->createLinkHierarchy($module); + $links['child-1']['plid'] = $links['child-2']['mlid']; + menu_link_save($links['child-1']); + + $expected_hierarchy = array( + 'parent' => FALSE, + 'child-1' => 'child-2', + 'child-1-1' => 'child-1', + 'child-1-2' => 'child-1', + 'child-2' => 'parent', + ); + $this->assertMenuLinkParents($links, $expected_hierarchy); + + // Start over, and delete child-1, and check that the children of child-1 + // have been reassigned to the parent. menu_link_delete() will cowardly + // refuse to delete a menu link defined by the system module, so skip the + // test in that case. + if ($module != 'system') { + $links = $this->createLinkHierarchy($module); + menu_link_delete($links['child-1']['mlid']); + + $expected_hierarchy = array( + 'parent' => FALSE, + 'child-1-1' => 'parent', + 'child-1-2' => 'parent', + 'child-2' => 'parent', + ); + $this->assertMenuLinkParents($links, $expected_hierarchy); + } + + // Start over, forcefully delete child-1 from the database, simulating a + // database crash. Check that the children of child-1 have been reassigned + // to the parent, going up on the old path hierarchy stored in each of the + // links. + $links = $this->createLinkHierarchy($module); + // Don't do that at home. + db_delete('menu_links') + ->condition('mlid', $links['child-1']['mlid']) + ->execute(); + + $expected_hierarchy = array( + 'parent' => FALSE, + 'child-1-1' => 'parent', + 'child-1-2' => 'parent', + 'child-2' => 'parent', + ); + $this->assertMenuLinkParents($links, $expected_hierarchy); + + // Start over, forcefully delete the parent from the database, simulating a + // database crash. Check that the children of parent are now top-level. + $links = $this->createLinkHierarchy($module); + // Don't do that at home. + db_delete('menu_links') + ->condition('mlid', $links['parent']['mlid']) + ->execute(); + + $expected_hierarchy = array( + 'child-1-1' => 'child-1', + 'child-1-2' => 'child-1', + 'child-2' => FALSE, + ); + $this->assertMenuLinkParents($links, $expected_hierarchy); + } + + /** + * Test automatic reparenting of menu links derived from menu routers. + */ + function testMenuLinkRouterReparenting() { + // Run all the standard parenting tests on menu links derived from + // menu routers. + $this->testMenuLinkReparenting('system'); + + // Additionnaly, test reparenting based on path. + $links = $this->createLinkHierarchy('system'); + + // Move child-1-2 has a child of child-2, making the link hierarchy + // inconsistent with the path hierarchy. + $links['child-1-2']['plid'] = $links['child-2']['mlid']; + menu_link_save($links['child-1-2']); + + // Check the new hierarchy. + $expected_hierarchy = array( + 'parent' => FALSE, + 'child-1' => 'parent', + 'child-1-1' => 'child-1', + 'child-2' => 'parent', + 'child-1-2' => 'child-2', + ); + $this->assertMenuLinkParents($links, $expected_hierarchy); + + // Now delete 'parent' directly from the database, simulating a database + // crash. 'child-1' and 'child-2' should get moved to the + // top-level. + // Don't do that at home. + db_delete('menu_links') + ->condition('mlid', $links['parent']['mlid']) + ->execute(); + $expected_hierarchy = array( + 'child-1' => FALSE, + 'child-1-1' => 'child-1', + 'child-2' => FALSE, + 'child-1-2' => 'child-2', + ); + $this->assertMenuLinkParents($links, $expected_hierarchy); + + // Now delete 'child-2' directly from the database, simulating a database + // crash. 'child-1-2' will get reparented under 'child-1' based on its + // path. + // Don't do that at home. + db_delete('menu_links') + ->condition('mlid', $links['child-2']['mlid']) + ->execute(); + $expected_hierarchy = array( + 'child-1' => FALSE, + 'child-1-1' => 'child-1', + 'child-1-2' => 'child-1', + ); + $this->assertMenuLinkParents($links, $expected_hierarchy); + } +} + +/** + * Tests rebuilding the menu by setting 'menu_rebuild_needed.' + */ +class MenuRebuildTestCase extends DrupalWebTestCase { + public static function getInfo() { + return array( + 'name' => 'Menu rebuild test', + 'description' => 'Test rebuilding of menu.', + 'group' => 'Menu', + ); + } + + /** + * Test if the 'menu_rebuild_needed' variable triggers a menu_rebuild() call. + */ + function testMenuRebuildByVariable() { + // Check if 'admin' path exists. + $admin_exists = db_query('SELECT path from {menu_router} WHERE path = :path', array(':path' => 'admin'))->fetchField(); + $this->assertEqual($admin_exists, 'admin', "The path 'admin/' exists prior to deleting."); + + // Delete the path item 'admin', and test that the path doesn't exist in the database. + $delete = db_delete('menu_router') + ->condition('path', 'admin') + ->execute(); + $admin_exists = db_query('SELECT path from {menu_router} WHERE path = :path', array(':path' => 'admin'))->fetchField(); + $this->assertFalse($admin_exists, "The path 'admin/' has been deleted and doesn't exist in the database."); + + // Now we enable the rebuild variable and trigger menu_execute_active_handler() + // to rebuild the menu item. Now 'admin' should exist. + variable_set('menu_rebuild_needed', TRUE); + // menu_execute_active_handler() should trigger the rebuild. + $this->drupalGet(''); + $admin_exists = db_query('SELECT path from {menu_router} WHERE path = :path', array(':path' => 'admin'))->fetchField(); + $this->assertEqual($admin_exists, 'admin', "The menu has been rebuilt, the path 'admin' now exists again."); + } + +} + +/** + * Menu tree data related tests. + */ +class MenuTreeDataTestCase extends DrupalUnitTestCase { + /** + * Dummy link structure acceptable for menu_tree_data(). + */ + var $links = array( + 1 => array('mlid' => 1, 'depth' => 1), + 2 => array('mlid' => 2, 'depth' => 1), + 3 => array('mlid' => 3, 'depth' => 2), + 4 => array('mlid' => 4, 'depth' => 3), + 5 => array('mlid' => 5, 'depth' => 1), + ); + + public static function getInfo() { + return array( + 'name' => 'Menu tree generation', + 'description' => 'Tests recursive menu tree generation functions.', + 'group' => 'Menu', + ); + } + + /** + * Validate the generation of a proper menu tree hierarchy. + */ + function testMenuTreeData() { + $tree = menu_tree_data($this->links); + + // Validate that parent items #1, #2, and #5 exist on the root level. + $this->assertSameLink($this->links[1], $tree[1]['link'], 'Parent item #1 exists.'); + $this->assertSameLink($this->links[2], $tree[2]['link'], 'Parent item #2 exists.'); + $this->assertSameLink($this->links[5], $tree[5]['link'], 'Parent item #5 exists.'); + + // Validate that child item #4 exists at the correct location in the hierarchy. + $this->assertSameLink($this->links[4], $tree[2]['below'][3]['below'][4]['link'], 'Child item #4 exists in the hierarchy.'); + } + + /** + * Check that two menu links are the same by comparing the mlid. + * + * @param $link1 + * A menu link item. + * @param $link2 + * A menu link item. + * @param $message + * The message to display along with the assertion. + * @return + * TRUE if the assertion succeeded, FALSE otherwise. + */ + protected function assertSameLink($link1, $link2, $message = '') { + return $this->assert($link1['mlid'] == $link2['mlid'], $message ? $message : 'First link is identical to second link'); + } +} + +/** + * Menu tree output related tests. + */ +class MenuTreeOutputTestCase extends DrupalWebTestCase { + /** + * Dummy link structure acceptable for menu_tree_output(). + */ + var $tree_data = array( + '1'=> array( + 'link' => array( 'menu_name' => 'main-menu', 'mlid' => 1, 'hidden'=>0, 'has_children' => 1, 'title' => 'Item 1', 'in_active_trail' => 1, 'access'=>1, 'href' => 'a', 'localized_options' => array('attributes' => array('title' =>'')) ), + 'below' => array( + '2' => array('link' => array( 'menu_name' => 'main-menu', 'mlid' => 2, 'hidden'=>0, 'has_children' => 1, 'title' => 'Item 2', 'in_active_trail' => 1, 'access'=>1, 'href' => 'a/b', 'localized_options' => array('attributes' => array('title' =>'')) ), + 'below' => array( + '3' => array('link' => array( 'menu_name' => 'main-menu', 'mlid' => 3, 'hidden'=>0, 'has_children' => 0, 'title' => 'Item 3', 'in_active_trail' => 0, 'access'=>1, 'href' => 'a/b/c', 'localized_options' => array('attributes' => array('title' =>'')) ), + 'below' => array() ), + '4' => array('link' => array( 'menu_name' => 'main-menu', 'mlid' => 4, 'hidden'=>0, 'has_children' => 0, 'title' => 'Item 4', 'in_active_trail' => 0, 'access'=>1, 'href' => 'a/b/d', 'localized_options' => array('attributes' => array('title' =>'')) ), + 'below' => array() ) + ) + ) + ) + ), + '5' => array('link' => array( 'menu_name' => 'main-menu', 'mlid' => 5, 'hidden'=>1, 'has_children' => 0, 'title' => 'Item 5', 'in_active_trail' => 0, 'access'=>1, 'href' => 'e', 'localized_options' => array('attributes' => array('title' =>'')) ), 'below' => array( ) ), + '6' => array('link' => array( 'menu_name' => 'main-menu', 'mlid' => 6, 'hidden'=>0, 'has_children' => 0, 'title' => 'Item 6', 'in_active_trail' => 0, 'access'=>0, 'href' => 'f', 'localized_options' => array('attributes' => array('title' =>'')) ), 'below' => array( ) ), + '7' => array('link' => array( 'menu_name' => 'main-menu', 'mlid' => 7, 'hidden'=>0, 'has_children' => 0, 'title' => 'Item 7', 'in_active_trail' => 0, 'access'=>1, 'href' => 'g', 'localized_options' => array('attributes' => array('title' =>'')) ), 'below' => array( ) ) + ); + + public static function getInfo() { + return array( + 'name' => 'Menu tree output', + 'description' => 'Tests menu tree output functions.', + 'group' => 'Menu', + ); + } + + function setUp() { + parent::setUp(); + } + + /** + * Validate the generation of a proper menu tree output. + */ + function testMenuTreeData() { + $output = menu_tree_output($this->tree_data); + + // Validate that the - in main-menu is changed into an underscore + $this->assertEqual($output['1']['#theme'], 'menu_link__main_menu', 'Hyphen is changed to an underscore on menu_link'); + $this->assertEqual($output['#theme_wrappers'][0], 'menu_tree__main_menu', 'Hyphen is changed to an underscore on menu_tree wrapper'); + // Looking for child items in the data + $this->assertEqual( $output['1']['#below']['2']['#href'], 'a/b', 'Checking the href on a child item'); + $this->assertTrue( in_array('active-trail',$output['1']['#below']['2']['#attributes']['class']) , 'Checking the active trail class'); + // Validate that the hidden and no access items are missing + $this->assertFalse( isset($output['5']), 'Hidden item should be missing'); + $this->assertFalse( isset($output['6']), 'False access should be missing'); + // Item 7 is after a couple hidden items. Just to make sure that 5 and 6 are skipped and 7 still included + $this->assertTrue( isset($output['7']), 'Item after hidden items is present'); + } +} + +/** + * Menu breadcrumbs related tests. + */ +class MenuBreadcrumbTestCase extends MenuWebTestCase { + public static function getInfo() { + return array( + 'name' => 'Breadcrumbs', + 'description' => 'Tests breadcrumbs functionality.', + 'group' => 'Menu', + ); + } + + function setUp() { + $modules = func_get_args(); + if (isset($modules[0]) && is_array($modules[0])) { + $modules = $modules[0]; + } + $modules[] = 'menu_test'; + parent::setUp($modules); + $perms = array_keys(module_invoke_all('permission')); + $this->admin_user = $this->drupalCreateUser($perms); + $this->drupalLogin($this->admin_user); + + // This test puts menu links in the Navigation menu and then tests for + // their presence on the page, so we need to ensure that the Navigation + // block will be displayed in all active themes. + db_update('block') + ->fields(array( + // Use a region that is valid for all themes. + 'region' => 'content', + 'status' => 1, + )) + ->condition('module', 'system') + ->condition('delta', 'navigation') + ->execute(); + } + + /** + * Tests breadcrumbs on node and administrative paths. + */ + function testBreadCrumbs() { + // Prepare common base breadcrumb elements. + $home = array('' => 'Home'); + $admin = $home + array('admin' => t('Administration')); + $config = $admin + array('admin/config' => t('Configuration')); + $type = 'article'; + $langcode = LANGUAGE_NONE; + + // Verify breadcrumbs for default local tasks. + $expected = array( + 'menu-test' => t('Menu test root'), + ); + $title = t('Breadcrumbs test: Local tasks'); + $trail = $home + $expected; + $tree = $expected + array( + 'menu-test/breadcrumb/tasks' => $title, + ); + $this->assertBreadcrumb('menu-test/breadcrumb/tasks', $trail, $title, $tree); + $this->assertBreadcrumb('menu-test/breadcrumb/tasks/first', $trail, $title, $tree); + $this->assertBreadcrumb('menu-test/breadcrumb/tasks/first/first', $trail, $title, $tree); + $trail += array( + 'menu-test/breadcrumb/tasks' => t('Breadcrumbs test: Local tasks'), + ); + $this->assertBreadcrumb('menu-test/breadcrumb/tasks/first/second', $trail, $title, $tree); + $this->assertBreadcrumb('menu-test/breadcrumb/tasks/second', $trail, $title, $tree); + $this->assertBreadcrumb('menu-test/breadcrumb/tasks/second/first', $trail, $title, $tree); + $trail += array( + 'menu-test/breadcrumb/tasks/second' => t('Second'), + ); + $this->assertBreadcrumb('menu-test/breadcrumb/tasks/second/second', $trail, $title, $tree); + + // Verify Taxonomy administration breadcrumbs. + $trail = $admin + array( + 'admin/structure' => t('Structure'), + ); + $this->assertBreadcrumb('admin/structure/taxonomy', $trail); + + $trail += array( + 'admin/structure/taxonomy' => t('Taxonomy'), + ); + $this->assertBreadcrumb('admin/structure/taxonomy/tags', $trail); + $trail += array( + 'admin/structure/taxonomy/tags' => t('Tags'), + ); + $this->assertBreadcrumb('admin/structure/taxonomy/tags/edit', $trail); + $this->assertBreadcrumb('admin/structure/taxonomy/tags/fields', $trail); + $this->assertBreadcrumb('admin/structure/taxonomy/tags/add', $trail); + + // Verify Menu administration breadcrumbs. + $trail = $admin + array( + 'admin/structure' => t('Structure'), + ); + $this->assertBreadcrumb('admin/structure/menu', $trail); + + $trail += array( + 'admin/structure/menu' => t('Menus'), + ); + $this->assertBreadcrumb('admin/structure/menu/manage/navigation', $trail); + $trail += array( + 'admin/structure/menu/manage/navigation' => t('Navigation'), + ); + $this->assertBreadcrumb("admin/structure/menu/item/6/edit", $trail); + $this->assertBreadcrumb('admin/structure/menu/manage/navigation/edit', $trail); + $this->assertBreadcrumb('admin/structure/menu/manage/navigation/add', $trail); + + // Verify Node administration breadcrumbs. + $trail = $admin + array( + 'admin/structure' => t('Structure'), + 'admin/structure/types' => t('Content types'), + ); + $this->assertBreadcrumb('admin/structure/types/add', $trail); + $this->assertBreadcrumb("admin/structure/types/manage/$type", $trail); + $trail += array( + "admin/structure/types/manage/$type" => t('Article'), + ); + $this->assertBreadcrumb("admin/structure/types/manage/$type/fields", $trail); + $this->assertBreadcrumb("admin/structure/types/manage/$type/display", $trail); + $trail_teaser = $trail + array( + "admin/structure/types/manage/$type/display" => t('Manage display'), + ); + $this->assertBreadcrumb("admin/structure/types/manage/$type/display/teaser", $trail_teaser); + $this->assertBreadcrumb("admin/structure/types/manage/$type/comment/fields", $trail); + $this->assertBreadcrumb("admin/structure/types/manage/$type/comment/display", $trail); + $this->assertBreadcrumb("admin/structure/types/manage/$type/delete", $trail); + $trail += array( + "admin/structure/types/manage/$type/fields" => t('Manage fields'), + ); + $this->assertBreadcrumb("admin/structure/types/manage/$type/fields/body", $trail); + $trail += array( + "admin/structure/types/manage/$type/fields/body" => t('Body'), + ); + $this->assertBreadcrumb("admin/structure/types/manage/$type/fields/body/widget-type", $trail); + + // Verify Filter text format administration breadcrumbs. + $format = db_query_range("SELECT format, name FROM {filter_format}", 1, 1)->fetch(); + $format_id = $format->format; + $trail = $config + array( + 'admin/config/content' => t('Content authoring'), + ); + $this->assertBreadcrumb('admin/config/content/formats', $trail); + + $trail += array( + 'admin/config/content/formats' => t('Text formats'), + ); + $this->assertBreadcrumb('admin/config/content/formats/add', $trail); + $this->assertBreadcrumb("admin/config/content/formats/$format_id", $trail); + $trail += array( + "admin/config/content/formats/$format_id" => $format->name, + ); + $this->assertBreadcrumb("admin/config/content/formats/$format_id/disable", $trail); + + // Verify node breadcrumbs (without menu link). + $node1 = $this->drupalCreateNode(); + $nid1 = $node1->nid; + $trail = $home; + $this->assertBreadcrumb("node/$nid1", $trail); + // Also verify that the node does not appear elsewhere (e.g., menu trees). + $this->assertNoLink($node1->title); + // The node itself should not be contained in the breadcrumb on the default + // local task, since there is no difference between both pages. + $this->assertBreadcrumb("node/$nid1/view", $trail); + // Also verify that the node does not appear elsewhere (e.g., menu trees). + $this->assertNoLink($node1->title); + + $trail += array( + "node/$nid1" => $node1->title, + ); + $this->assertBreadcrumb("node/$nid1/edit", $trail); + + // Verify that breadcrumb on node listing page contains "Home" only. + $trail = array(); + $this->assertBreadcrumb('node', $trail); + + // Verify node breadcrumbs (in menu). + // Do this separately for Main menu and Navigation menu, since only the + // latter is a preferred menu by default. + // @todo Also test all themes? Manually testing led to the suspicion that + // breadcrumbs may differ, possibly due to template.php overrides. + $menus = array('main-menu', 'navigation'); + // Alter node type menu settings. + variable_set("menu_options_$type", $menus); + variable_set("menu_parent_$type", 'navigation:0'); + + foreach ($menus as $menu) { + // Create a parent node in the current menu. + $title = $this->randomName(); + $node2 = $this->drupalCreateNode(array( + 'type' => $type, + 'title' => $title, + 'menu' => array( + 'enabled' => 1, + 'link_title' => 'Parent ' . $title, + 'description' => '', + 'menu_name' => $menu, + 'plid' => 0, + ), + )); + $nid2 = $node2->nid; + + $trail = $home; + $tree = array( + "node/$nid2" => $node2->menu['link_title'], + ); + $this->assertBreadcrumb("node/$nid2", $trail, $node2->title, $tree); + // The node itself should not be contained in the breadcrumb on the + // default local task, since there is no difference between both pages. + $this->assertBreadcrumb("node/$nid2/view", $trail, $node2->title, $tree); + $trail += array( + "node/$nid2" => $node2->menu['link_title'], + ); + $this->assertBreadcrumb("node/$nid2/edit", $trail); + + // Create a child node in the current menu. + $title = $this->randomName(); + $node3 = $this->drupalCreateNode(array( + 'type' => $type, + 'title' => $title, + 'menu' => array( + 'enabled' => 1, + 'link_title' => 'Child ' . $title, + 'description' => '', + 'menu_name' => $menu, + 'plid' => $node2->menu['mlid'], + ), + )); + $nid3 = $node3->nid; + + $this->assertBreadcrumb("node/$nid3", $trail, $node3->title, $tree, FALSE); + // The node itself should not be contained in the breadcrumb on the + // default local task, since there is no difference between both pages. + $this->assertBreadcrumb("node/$nid3/view", $trail, $node3->title, $tree, FALSE); + $trail += array( + "node/$nid3" => $node3->menu['link_title'], + ); + $tree += array( + "node/$nid3" => $node3->menu['link_title'], + ); + $this->assertBreadcrumb("node/$nid3/edit", $trail); + + // Verify that node listing page still contains "Home" only. + $trail = array(); + $this->assertBreadcrumb('node', $trail); + + if ($menu == 'navigation') { + $parent = $node2; + $child = $node3; + } + } + + // Create a Navigation menu link for 'node', move the last parent node menu + // link below it, and verify a full breadcrumb for the last child node. + $menu = 'navigation'; + $edit = array( + 'link_title' => 'Root', + 'link_path' => 'node', + ); + $this->drupalPost("admin/structure/menu/manage/$menu/add", $edit, t('Save')); + $link = db_query('SELECT * FROM {menu_links} WHERE link_title = :title', array(':title' => 'Root'))->fetchAssoc(); + + $edit = array( + 'menu[parent]' => $link['menu_name'] . ':' . $link['mlid'], + ); + $this->drupalPost("node/{$parent->nid}/edit", $edit, t('Save')); + $expected = array( + "node" => $link['link_title'], + ); + $trail = $home + $expected; + $tree = $expected + array( + "node/{$parent->nid}" => $parent->menu['link_title'], + ); + $this->assertBreadcrumb(NULL, $trail, $parent->title, $tree); + $trail += array( + "node/{$parent->nid}" => $parent->menu['link_title'], + ); + $tree += array( + "node/{$child->nid}" => $child->menu['link_title'], + ); + $this->assertBreadcrumb("node/{$child->nid}", $trail, $child->title, $tree); + + // Add a taxonomy term/tag to last node, and add a link for that term to the + // Navigation menu. + $tags = array( + 'Drupal' => array(), + 'Breadcrumbs' => array(), + ); + $edit = array( + "field_tags[$langcode]" => implode(',', array_keys($tags)), + ); + $this->drupalPost("node/{$parent->nid}/edit", $edit, t('Save')); + + // Put both terms into a hierarchy Drupal » Breadcrumbs. Required for both + // the menu links and the terms itself, since taxonomy_term_page() resets + // the breadcrumb based on taxonomy term hierarchy. + $parent_tid = 0; + foreach ($tags as $name => $null) { + $terms = taxonomy_term_load_multiple(NULL, array('name' => $name)); + $term = reset($terms); + $tags[$name]['term'] = $term; + if ($parent_tid) { + $edit = array( + 'parent[]' => array($parent_tid), + ); + $this->drupalPost("taxonomy/term/{$term->tid}/edit", $edit, t('Save')); + } + $parent_tid = $term->tid; + } + $parent_mlid = 0; + foreach ($tags as $name => $data) { + $term = $data['term']; + $edit = array( + 'link_title' => "$name link", + 'link_path' => "taxonomy/term/{$term->tid}", + 'parent' => "$menu:{$parent_mlid}", + ); + $this->drupalPost("admin/structure/menu/manage/$menu/add", $edit, t('Save')); + $tags[$name]['link'] = db_query('SELECT * FROM {menu_links} WHERE link_title = :title AND link_path = :href', array( + ':title' => $edit['link_title'], + ':href' => $edit['link_path'], + ))->fetchAssoc(); + $tags[$name]['link']['link_path'] = $edit['link_path']; + $parent_mlid = $tags[$name]['link']['mlid']; + } + + // Verify expected breadcrumbs for menu links. + $trail = $home; + $tree = array(); + foreach ($tags as $name => $data) { + $term = $data['term']; + $link = $data['link']; + + $tree += array( + $link['link_path'] => $link['link_title'], + ); + $this->assertBreadcrumb($link['link_path'], $trail, $term->name, $tree); + $this->assertRaw(check_plain($parent->title), 'Tagged node found.'); + + // Additionally make sure that this link appears only once; i.e., the + // untranslated menu links automatically generated from menu router items + // ('taxonomy/term/%') should never be translated and appear in any menu + // other than the breadcrumb trail. + $elements = $this->xpath('//div[@id=:menu]/descendant::a[@href=:href]', array( + ':menu' => 'block-system-navigation', + ':href' => url($link['link_path']), + )); + $this->assertTrue(count($elements) == 1, "Link to {$link['link_path']} appears only once."); + + // Next iteration should expect this tag as parent link. + // Note: Term name, not link name, due to taxonomy_term_page(). + $trail += array( + $link['link_path'] => $term->name, + ); + } + + // Verify breadcrumbs on user and user/%. + // We need to log back in and out below, and cannot simply grant the + // 'administer users' permission, since user_page() makes your head explode. + user_role_grant_permissions(DRUPAL_ANONYMOUS_RID, array( + 'access user profiles', + )); + $this->drupalLogout(); + + // Verify breadcrumb on front page. + $this->assertBreadcrumb('', array()); + + // Verify breadcrumb on user pages (without menu link) for anonymous user. + $trail = $home; + $this->assertBreadcrumb('user', $trail, t('User account')); + $this->assertBreadcrumb('user/' . $this->admin_user->uid, $trail, $this->admin_user->name); + + // Verify breadcrumb on user pages (without menu link) for registered users. + $this->drupalLogin($this->admin_user); + $trail = $home; + $this->assertBreadcrumb('user', $trail, $this->admin_user->name); + $this->assertBreadcrumb('user/' . $this->admin_user->uid, $trail, $this->admin_user->name); + $trail += array( + 'user/' . $this->admin_user->uid => $this->admin_user->name, + ); + $this->assertBreadcrumb('user/' . $this->admin_user->uid . '/edit', $trail, $this->admin_user->name); + + // Create a second user to verify breadcrumb on user pages again. + $this->web_user = $this->drupalCreateUser(array( + 'administer users', + 'access user profiles', + )); + $this->drupalLogin($this->web_user); + + // Verify correct breadcrumb and page title on another user's account pages + // (without menu link). + $trail = $home; + $this->assertBreadcrumb('user/' . $this->admin_user->uid, $trail, $this->admin_user->name); + $trail += array( + 'user/' . $this->admin_user->uid => $this->admin_user->name, + ); + $this->assertBreadcrumb('user/' . $this->admin_user->uid . '/edit', $trail, $this->admin_user->name); + + // Verify correct breadcrumb and page title when viewing own user account + // pages (without menu link). + $trail = $home; + $this->assertBreadcrumb('user/' . $this->web_user->uid, $trail, $this->web_user->name); + $trail += array( + 'user/' . $this->web_user->uid => $this->web_user->name, + ); + $this->assertBreadcrumb('user/' . $this->web_user->uid . '/edit', $trail, $this->web_user->name); + + // Add a Navigation menu links for 'user' and $this->admin_user. + // Although it may be faster to manage these links via low-level API + // functions, there's a lot that can go wrong in doing so. + $this->drupalLogin($this->admin_user); + $edit = array( + 'link_title' => 'User', + 'link_path' => 'user', + ); + $this->drupalPost("admin/structure/menu/manage/$menu/add", $edit, t('Save')); + $link_user = db_query('SELECT * FROM {menu_links} WHERE link_title = :title AND link_path = :href', array( + ':title' => $edit['link_title'], + ':href' => $edit['link_path'], + ))->fetchAssoc(); + + $edit = array( + 'link_title' => $this->admin_user->name . ' link', + 'link_path' => 'user/' . $this->admin_user->uid, + ); + $this->drupalPost("admin/structure/menu/manage/$menu/add", $edit, t('Save')); + $link_admin_user = db_query('SELECT * FROM {menu_links} WHERE link_title = :title AND link_path = :href', array( + ':title' => $edit['link_title'], + ':href' => $edit['link_path'], + ))->fetchAssoc(); + + // Verify expected breadcrumbs for the two separate links. + $this->drupalLogout(); + $trail = $home; + $tree = array( + $link_user['link_path'] => $link_user['link_title'], + ); + $this->assertBreadcrumb('user', $trail, $link_user['link_title'], $tree); + $tree = array( + $link_admin_user['link_path'] => $link_admin_user['link_title'], + ); + $this->assertBreadcrumb('user/' . $this->admin_user->uid, $trail, $link_admin_user['link_title'], $tree); + + $this->drupalLogin($this->admin_user); + $trail += array( + $link_admin_user['link_path'] => $link_admin_user['link_title'], + ); + $this->assertBreadcrumb('user/' . $this->admin_user->uid . '/edit', $trail, $link_admin_user['link_title'], $tree, FALSE); + + // Move 'user/%' below 'user' and verify again. + $edit = array( + 'parent' => "$menu:{$link_user['mlid']}", + ); + $this->drupalPost("admin/structure/menu/item/{$link_admin_user['mlid']}/edit", $edit, t('Save')); + + $this->drupalLogout(); + $trail = $home; + $tree = array( + $link_user['link_path'] => $link_user['link_title'], + ); + $this->assertBreadcrumb('user', $trail, $link_user['link_title'], $tree); + $trail += array( + $link_user['link_path'] => $link_user['link_title'], + ); + $tree += array( + $link_admin_user['link_path'] => $link_admin_user['link_title'], + ); + $this->assertBreadcrumb('user/' . $this->admin_user->uid, $trail, $link_admin_user['link_title'], $tree); + + $this->drupalLogin($this->admin_user); + $trail += array( + $link_admin_user['link_path'] => $link_admin_user['link_title'], + ); + $this->assertBreadcrumb('user/' . $this->admin_user->uid . '/edit', $trail, $link_admin_user['link_title'], $tree, FALSE); + + // Create an only slightly privileged user being able to access site reports + // but not administration pages. + $this->web_user = $this->drupalCreateUser(array( + 'access site reports', + )); + $this->drupalLogin($this->web_user); + + // Verify that we can access recent log entries, there is a corresponding + // page title, and that the breadcrumb is empty (because the user is not + // able to access "Administer", so the trail cannot recurse into it). + $trail = array(); + $this->assertBreadcrumb('admin', $trail, t('Access denied')); + $this->assertResponse(403); + + $trail = $home; + $this->assertBreadcrumb('admin/reports', $trail, t('Reports')); + $this->assertNoResponse(403); + + $this->assertBreadcrumb('admin/reports/dblog', $trail, t('Recent log messages')); + $this->assertNoResponse(403); + } +} + +/** + * Tests active menu trails. + */ +class MenuTrailTestCase extends MenuWebTestCase { + public static function getInfo() { + return array( + 'name' => 'Active trail', + 'description' => 'Tests active menu trails and alteration functionality.', + 'group' => 'Menu', + ); + } + + function setUp() { + $modules = func_get_args(); + if (isset($modules[0]) && is_array($modules[0])) { + $modules = $modules[0]; + } + $modules[] = 'menu_test'; + parent::setUp($modules); + $this->admin_user = $this->drupalCreateUser(array('administer site configuration', 'access administration pages')); + $this->drupalLogin($this->admin_user); + + // This test puts menu links in the Navigation menu and then tests for + // their presence on the page, so we need to ensure that the Navigation + // block will be displayed in all active themes. + db_update('block') + ->fields(array( + // Use a region that is valid for all themes. + 'region' => 'content', + 'status' => 1, + )) + ->condition('module', 'system') + ->condition('delta', 'navigation') + ->execute(); + + // This test puts menu links in the Management menu and then tests for + // their presence on the page, so we need to ensure that the Management + // block will be displayed in all active themes. + db_update('block') + ->fields(array( + // Use a region that is valid for all themes. + 'region' => 'content', + 'status' => 1, + )) + ->condition('module', 'system') + ->condition('delta', 'management') + ->execute(); + } + + /** + * Tests active trails are properly affected by menu_tree_set_path(). + */ + function testMenuTreeSetPath() { + $home = array('' => 'Home'); + $config_tree = array( + 'admin' => t('Administration'), + 'admin/config' => t('Configuration'), + ); + $config = $home + $config_tree; + + // The menu_test_menu_tree_set_path system variable controls whether or not + // the menu_test_menu_trail_callback() callback (used by all paths in these + // tests) issues an overriding call to menu_trail_set_path(). + $test_menu_path = array( + 'menu_name' => 'management', + 'path' => 'admin/config/system/site-information', + ); + + $breadcrumb = $home + array( + 'menu-test' => t('Menu test root'), + ); + $tree = array( + 'menu-test' => t('Menu test root'), + 'menu-test/menu-trail' => t('Menu trail - Case 1'), + ); + + // Test the tree generation for the Navigation menu. + variable_del('menu_test_menu_tree_set_path'); + $this->assertBreadcrumb('menu-test/menu-trail', $breadcrumb, t('Menu trail - Case 1'), $tree); + + // Override the active trail for the Management tree; it should not affect + // the Navigation tree. + variable_set('menu_test_menu_tree_set_path', $test_menu_path); + $this->assertBreadcrumb('menu-test/menu-trail', $breadcrumb, t('Menu trail - Case 1'), $tree); + + $breadcrumb = $config + array( + 'admin/config/development' => t('Development'), + ); + $tree = $config_tree + array( + 'admin/config/development' => t('Development'), + 'admin/config/development/menu-trail' => t('Menu trail - Case 2'), + ); + + $override_breadcrumb = $config + array( + 'admin/config/system' => t('System'), + 'admin/config/system/site-information' => t('Site information'), + ); + $override_tree = $config_tree + array( + 'admin/config/system' => t('System'), + 'admin/config/system/site-information' => t('Site information'), + ); + + // Test the tree generation for the Management menu. + variable_del('menu_test_menu_tree_set_path'); + $this->assertBreadcrumb('admin/config/development/menu-trail', $breadcrumb, t('Menu trail - Case 2'), $tree); + + // Override the active trail for the Management tree; it should affect the + // breadcrumbs and Management tree. + variable_set('menu_test_menu_tree_set_path', $test_menu_path); + $this->assertBreadcrumb('admin/config/development/menu-trail', $override_breadcrumb, t('Menu trail - Case 2'), $override_tree); + } + + /** + * Tests that the active trail works correctly on custom 403 and 404 pages. + */ + function testCustom403And404Pages() { + // Set the custom 403 and 404 pages we will use. + variable_set('site_403', 'menu-test/custom-403-page'); + variable_set('site_404', 'menu-test/custom-404-page'); + + // Define the paths we'll visit to trigger 403 and 404 responses during + // this test, and the expected active trail for each case. + $paths = array( + 403 => 'admin/config', + 404 => $this->randomName(), + ); + // For the 403 page, the initial trail during the Drupal bootstrap should + // include the page that the user is trying to visit, while the final trail + // should reflect the custom 403 page that the user was redirected to. + $expected_trail[403]['initial'] = array( + '' => 'Home', + 'admin/config' => 'Configuration', + ); + $expected_trail[403]['final'] = array( + '' => 'Home', + 'menu-test' => 'Menu test root', + 'menu-test/custom-403-page' => 'Custom 403 page', + ); + // For the 404 page, the initial trail during the Drupal bootstrap should + // only contain the link back to "Home" (since the page the user is trying + // to visit doesn't have any menu items associated with it), while the + // final trail should reflect the custom 404 page that the user was + // redirected to. + $expected_trail[404]['initial'] = array( + '' => 'Home', + ); + $expected_trail[404]['final'] = array( + '' => 'Home', + 'menu-test' => 'Menu test root', + 'menu-test/custom-404-page' => 'Custom 404 page', + ); + + // Visit each path as an anonymous user so that we will actually get a 403 + // on admin/config. + $this->drupalLogout(); + foreach (array(403, 404) as $status_code) { + // Before visiting the page, trigger the code in the menu_test module + // that will record the active trail (so we can check it in this test). + variable_set('menu_test_record_active_trail', TRUE); + $this->drupalGet($paths[$status_code]); + $this->assertResponse($status_code); + + // Check that the initial trail (during the Drupal bootstrap) matches + // what we expect. + $initial_trail = variable_get('menu_test_active_trail_initial', array()); + $this->assertEqual(count($initial_trail), count($expected_trail[$status_code]['initial']), format_string('The initial active trail for a @status_code page contains the expected number of items (expected: @expected, found: @found).', array( + '@status_code' => $status_code, + '@expected' => count($expected_trail[$status_code]['initial']), + '@found' => count($initial_trail), + ))); + foreach (array_keys($expected_trail[$status_code]['initial']) as $index => $path) { + $this->assertEqual($initial_trail[$index]['href'], $path, format_string('Element number @number of the initial active trail for a @status_code page contains the correct path (expected: @expected, found: @found)', array( + '@number' => $index + 1, + '@status_code' => $status_code, + '@expected' => $path, + '@found' => $initial_trail[$index]['href'], + ))); + } + + // Check that the final trail (after the user has been redirected to the + // custom 403/404 page) matches what we expect. + $final_trail = variable_get('menu_test_active_trail_final', array()); + $this->assertEqual(count($final_trail), count($expected_trail[$status_code]['final']), format_string('The final active trail for a @status_code page contains the expected number of items (expected: @expected, found: @found).', array( + '@status_code' => $status_code, + '@expected' => count($expected_trail[$status_code]['final']), + '@found' => count($final_trail), + ))); + foreach (array_keys($expected_trail[$status_code]['final']) as $index => $path) { + $this->assertEqual($final_trail[$index]['href'], $path, format_string('Element number @number of the final active trail for a @status_code page contains the correct path (expected: @expected, found: @found)', array( + '@number' => $index + 1, + '@status_code' => $status_code, + '@expected' => $path, + '@found' => $final_trail[$index]['href'], + ))); + } + + // Check that the breadcrumb displayed on the final custom 403/404 page + // matches what we expect. (The last item of the active trail represents + // the current page, which is not supposed to appear in the breadcrumb, + // so we need to remove it from the array before checking.) + array_pop($expected_trail[$status_code]['final']); + $this->assertBreadcrumb(NULL, $expected_trail[$status_code]['final']); + } + } +}