from django.test import RequestFactory, TestCase from django.urls import reverse from django.utils import translation from wagtail import hooks from wagtail.admin.menu import ( AdminOnlyMenuItem, DismissibleMenuItem, DismissibleSubmenuMenuItem, Menu, MenuItem, SubmenuMenuItem, admin_menu, ) from wagtail.admin.ui import sidebar from wagtail.test.utils import WagtailTestUtils from wagtail.users.models import UserProfile def menu_item_hook(*args, cls=MenuItem, **kwargs): def hook_fn(): return cls(*args, **kwargs) return hook_fn class TestMenuRendering(WagtailTestUtils, TestCase): def setUp(self): self.request = RequestFactory().get("/admin") self.request.user = self.create_superuser(username="admin") self.profile = UserProfile.get_for_user(self.request.user) self.user = self.login() def test_remember_collapsed(self): """Sidebar should render with collapsed class applied.""" # Sidebar should not be collapsed self.client.cookies["wagtail_sidebar_collapsed"] = "0" response = self.client.get(reverse("wagtailadmin_home")) self.assertNotContains(response, "sidebar-collapsed") # Sidebar should be collapsed self.client.cookies["wagtail_sidebar_collapsed"] = "1" response = self.client.get(reverse("wagtailadmin_home")) self.assertContains(response, "sidebar-collapsed") def test_simple_menu(self): # Note: initialise the menu before registering hooks as this is what happens in reality. # (the real menus are initialised at the module level in admin/menu.py) menu = Menu(register_hook_name="register_menu_item") with hooks.register_temporarily( [ ("register_menu_item", menu_item_hook("Pages", "/pages/")), ("register_menu_item", menu_item_hook("Images", "/images/")), ] ): rendered = menu.render_component(self.request) self.assertIsInstance(rendered, list) self.assertListEqual( rendered, [ sidebar.LinkMenuItem("pages", "Pages", "/pages/"), sidebar.LinkMenuItem("images", "Images", "/images/"), ], ) def test_menu_with_construct_hook(self): menu = Menu( register_hook_name="register_menu_item", construct_hook_name="construct_menu", ) def remove_images(request, items): items[:] = [item for item in items if not item.name == "images"] with hooks.register_temporarily( [ ("register_menu_item", menu_item_hook("Pages", "/pages/")), ("register_menu_item", menu_item_hook("Images", "/images/")), ("construct_menu", remove_images), ] ): rendered = menu.render_component(self.request) self.assertEqual( rendered, [ sidebar.LinkMenuItem("pages", "Pages", "/pages/"), ], ) def test_submenu(self): menu = Menu(register_hook_name="register_menu_item") submenu = Menu(register_hook_name="register_submenu_item") with hooks.register_temporarily( [ ( "register_menu_item", menu_item_hook("My lovely submenu", submenu, cls=SubmenuMenuItem), ), ("register_submenu_item", menu_item_hook("Pages", "/pages/")), ] ): rendered = menu.render_component(self.request) self.assertIsInstance(rendered, list) self.assertEqual(len(rendered), 1) self.assertIsInstance(rendered[0], sidebar.SubMenuItem) self.assertEqual(rendered[0].name, "my-lovely-submenu") self.assertEqual(rendered[0].label, "My lovely submenu") self.assertListEqual( rendered[0].menu_items, [ sidebar.LinkMenuItem("pages", "Pages", "/pages/"), ], ) def test_dismissible_initial(self): menu = Menu(register_hook_name="register_menu_item") submenu = Menu(register_hook_name="register_submenu_item") with hooks.register_temporarily( [ ( "register_menu_item", menu_item_hook( "My dismissible submenu", submenu, cls=DismissibleSubmenuMenuItem, name="dismissible-submenu-menu-item", ), ), ( "register_submenu_item", menu_item_hook( "Pages", "/pages/", cls=DismissibleMenuItem, name="dismissible-menu-item", ), ), ] ): rendered = menu.render_component(self.request) self.assertIsInstance(rendered, list) self.assertEqual(len(rendered), 1) self.assertIsInstance(rendered[0], sidebar.SubMenuItem) self.assertEqual(rendered[0].name, "dismissible-submenu-menu-item") self.assertEqual(rendered[0].label, "My dismissible submenu") self.assertEqual( rendered[0].attrs, # Should not be dismissed { "data-controller": "w-dismissible", "data-w-dismissible-dismissed-class": "w-dismissible--dismissed", "data-w-dismissible-id-value": "dismissible-submenu-menu-item", }, ) self.assertListEqual( rendered[0].menu_items, [ sidebar.LinkMenuItem( "dismissible-menu-item", "Pages", "/pages/", # Should not be dismissed attrs={ "data-controller": "w-dismissible", "data-w-dismissible-dismissed-class": "w-dismissible--dismissed", "data-w-dismissible-id-value": "dismissible-menu-item", }, ), ], ) def test_dismissible_dismissed(self): self.profile.dismissibles = { "dismissible-submenu-menu-item": True, "dismissible-menu-item": True, } self.profile.save() self.request.user.refresh_from_db() menu = Menu(register_hook_name="register_menu_item") submenu = Menu(register_hook_name="register_submenu_item") with hooks.register_temporarily( [ ( "register_menu_item", menu_item_hook( "My dismissible submenu", submenu, cls=DismissibleSubmenuMenuItem, name="dismissible-submenu-menu-item", ), ), ( "register_submenu_item", menu_item_hook( "Pages", "/pages/", cls=DismissibleMenuItem, name="dismissible-menu-item", ), ), ] ): rendered = menu.render_component(self.request) self.assertIsInstance(rendered, list) self.assertEqual(len(rendered), 1) self.assertIsInstance(rendered[0], sidebar.SubMenuItem) self.assertEqual(rendered[0].name, "dismissible-submenu-menu-item") self.assertEqual(rendered[0].label, "My dismissible submenu") self.assertEqual( rendered[0].attrs, { "data-controller": "w-dismissible", "data-w-dismissible-dismissed-class": "w-dismissible--dismissed", "data-w-dismissible-id-value": "dismissible-submenu-menu-item", # Should be dismissed "data-w-dismissible-dismissed-value": "true", }, ) self.assertListEqual( rendered[0].menu_items, [ sidebar.LinkMenuItem( "dismissible-menu-item", "Pages", "/pages/", # Should be dismissed attrs={ "data-controller": "w-dismissible", "data-w-dismissible-dismissed-class": "w-dismissible--dismissed", "data-w-dismissible-id-value": "dismissible-menu-item", "data-w-dismissible-dismissed-value": "true", }, ), ], ) def test_dismissible_no_userprofile(self): # Without a user profile, dismissible menu items should not be dismissed self.profile.delete() self.request.user.refresh_from_db() menu = Menu(register_hook_name="register_menu_item") submenu = Menu(register_hook_name="register_submenu_item") with hooks.register_temporarily( [ ( "register_menu_item", menu_item_hook( "My dismissible submenu", submenu, cls=DismissibleSubmenuMenuItem, name="dismissible-submenu-menu-item", ), ), ( "register_submenu_item", menu_item_hook( "Pages", "/pages/", cls=DismissibleMenuItem, name="dismissible-menu-item", ), ), ] ): rendered = menu.render_component(self.request) self.assertIsInstance(rendered, list) self.assertEqual(len(rendered), 1) self.assertIsInstance(rendered[0], sidebar.SubMenuItem) self.assertEqual(rendered[0].name, "dismissible-submenu-menu-item") self.assertEqual(rendered[0].label, "My dismissible submenu") self.assertEqual( rendered[0].attrs, { "data-controller": "w-dismissible", "data-w-dismissible-dismissed-class": "w-dismissible--dismissed", "data-w-dismissible-id-value": "dismissible-submenu-menu-item", }, ) self.assertListEqual( rendered[0].menu_items, [ sidebar.LinkMenuItem( "dismissible-menu-item", "Pages", "/pages/", attrs={ "data-controller": "w-dismissible", "data-w-dismissible-dismissed-class": "w-dismissible--dismissed", "data-w-dismissible-id-value": "dismissible-menu-item", }, ), ], ) def test_admin_only_menuitem(self): menu = Menu(register_hook_name="register_menu_item") with hooks.register_temporarily( [ ("register_menu_item", menu_item_hook("Pages", "/pages/")), ( "register_menu_item", menu_item_hook( "Secret pages", "/pages/secret/", cls=AdminOnlyMenuItem ), ), ] ): rendered = menu.render_component(self.request) self.request.user = self.create_user(username="non-admin") rendered_non_admin = menu.render_component(self.request) self.assertListEqual( rendered, [ sidebar.LinkMenuItem("pages", "Pages", "/pages/"), sidebar.LinkMenuItem("secret-pages", "Secret pages", "/pages/secret/"), ], ) self.assertListEqual( rendered_non_admin, [ sidebar.LinkMenuItem("pages", "Pages", "/pages/"), ], ) def test_menu_items_have_names(self): # Delete the registered_menu_items cache try: del admin_menu.registered_menu_items # The cache may not be created yet if the test is run in isolation except AttributeError: pass # Generate the menu items using a different language with translation.override("fr"): names = {item.name for item in admin_menu.registered_menu_items} # Default menu items expected = { "explorer", "images", "documents", "snippets", "forms", "reports", "settings", "help", } # If some of the above items do not have a name, they will be # automatically generated from the label, which is translatable. # We want the name to be consistent across languages, so this test will # fail if the label is translated. self.assertFalse(expected - names) def test_submenu_items_have_names(self): names = set() # Generate the submenu items using a different language with translation.override("fr"): # Getting only the submenu items for item in admin_menu.registered_menu_items: if not hasattr(item, "menu"): continue # Delete the registered_menu_items cache try: del item.menu.registered_menu_items # The cache may not be created yet if the test is run in isolation except AttributeError: pass for subitem in item.menu.registered_menu_items: names.add(subitem.name) # Default submenu items expected = { "site-history", "workflows", "users", "revisable-child-models", "groups", "aging-pages", "editor-guide", "sites", "publishables", "locked-pages", "workflow-tasks", "icon-site-setting", "revisable-models", "collections", "locales", "styleguide", "test-generic-setting", "test-site-setting", "important-pages-generic-setting", "redirects", "important-pages-site-setting", "workflow-tasks", "promoted-search-results", "icon-generic-setting", "file-site-setting", "workflows", "file-generic-setting", } # If some of the above subitems do not have a name, they will be # automatically generated from the label, which is translatable. # We want the name to be consistent across languages, so this test will # fail if the label is translated. self.assertFalse(expected - names)