#!/usr/bin/python3
# SPDX-FileCopyrightText: 2023-2025 Univention GmbH
# SPDX-License-Identifier: AGPL-3.0-only
from dataclasses import dataclass
from playwright.sync_api import Locator, Page, expect
from univention.config_registry import ucr
from univention.lib.i18n import Translation
from univention.testing.browser.lib import UMCBrowserTest
_ = Translation('ucs-test-framework').translate
[docs]
@dataclass
class CreatedItem:
identifying_name: str
[docs]
class AddObjectDialog:
"""
Use this class when pressing the "Add" button opens a dialog
:param tester: The UMCBrowserTest instance to use
:param locator: The locator of the dialog
"""
def __init__(self, tester: UMCBrowserTest, locator: Locator) -> None:
self.tester = tester
self.locator: Locator = locator
self.page: Page = tester.page
[docs]
def fill_field(self, label: str, value: str, exact: bool = False, **kwargs) -> None:
self.locator.get_by_role('textbox', name=label, exact=exact, **kwargs).fill(value)
[docs]
def finish(self, label: str) -> None:
self.locator.get_by_role('button', name=label).click()
self.locator.get_by_role('button', name=_('Cancel')).last.click()
expect(self.locator).to_be_hidden()
[docs]
def next(self, label: str = _('Next')) -> None:
self.locator.get_by_role('button', name=label).click()
[docs]
class DetailsView:
def __init__(self, tester: UMCBrowserTest) -> None:
self.tester = tester
self.page = tester.page
[docs]
def fill_field(self, label: str, value: str) -> None:
self.page.get_by_label(label).fill(value)
[docs]
def check_checkbox(self, label: str) -> None:
self.page.get_by_role('checkbox', name=label).check()
[docs]
def save(self, label: str = _('Save')) -> None:
self.page.get_by_role('button', name=label).click()
[docs]
def open_tab(self, name: str) -> None:
self.page.get_by_role('tab', name=name).click()
[docs]
def upload_picture(self, img_path: str) -> Locator:
# for some reason this button is a textbox and not a button
upload_profile_picture_button = self.page.get_by_role('textbox', name=_('Upload profile image'))
expect(upload_profile_picture_button).to_be_visible()
self.page.screenshot(path=img_path)
with self.page.expect_file_chooser() as file_chooser_info:
upload_profile_picture_button.click()
file_chooser = file_chooser_info.value
file_chooser.set_files(img_path)
# very ugly locator for this but the image isn't even in an <img> tag
image_locator = self.page.locator('.umcUDMUsersModule__jpegPhoto .umcImage__img')
expect(image_locator).to_be_visible()
return image_locator
[docs]
def remove_picture(self) -> None:
# for some reason this locator resolves to two buttons
remove_button = self.page.get_by_role('button', name='Remove').first
expect(remove_button).to_be_visible()
remove_button.click()
[docs]
class GenericUDMModule:
"""
The GenericUmcModule is the base class for a bunch of UMC Modules which are all structured similarly
This class provides a bunch of methods for functionality that is similar/common in the modules.
:param tester: The base tester
:param module_name: The module name to be opened by navigate
"""
def __init__(self, tester: UMCBrowserTest, module_name: str) -> None:
self.tester: UMCBrowserTest = tester
self.page: Page = tester.page
self.module_name: str = module_name
[docs]
def navigate(self, username=ucr.get('tests/domainadmin/username', 'Administrator'), password=ucr.get('tests/domainadmin/pwd', 'univention')) -> None:
self.tester.login(username, password)
self.tester.open_module(self.module_name)
[docs]
def add_object_dialog(self) -> AddObjectDialog:
"""
Will add an object by clicking the `Add` button which is visible for all modules inheriting from this class
The way how objects are added is however different between the classes. Some open a dialog to fill in information,
others open a full page view and others do both. This function should be used when a dialog is opened by clicking the add button.
If there is a full page view being opened `add_object_detail_view` should be used
"""
self.page.get_by_role('button', name=_('Add')).click()
if self.page.get_by_text(_('This UCS system is part of an Active Directory domain')).is_visible():
self.page.get_by_role('button', name=_('Next')).click()
return AddObjectDialog(self.tester, self.page.get_by_role('dialog'))
[docs]
def add_object_detail_view(self) -> DetailsView:
"""See `add_object_dialog` for details"""
self.page.get_by_role('button', name=_('Add')).click()
if self.page.get_by_text(_('This UCS system is part of an Active Directory domain')).is_visible():
self.page.get_by_role('button', name=_('Next')).click()
return DetailsView(self.tester)
[docs]
def open_details(self, name: str) -> DetailsView:
"""Click on the `name` of a <tr> entry to open it's DetailsView"""
self.page.get_by_role('gridcell').get_by_text(name).click()
return DetailsView(self.tester)
[docs]
def delete(self, name: str | CreatedItem) -> None:
"""Checks the checkbox of the row containing `name` and then press the delete button"""
if isinstance(name, CreatedItem):
name = name.identifying_name
self.tester.check_checkbox_in_grid_by_name(name)
self.page.get_by_role('button', name=_('Delete')).click()
self.page.get_by_role('dialog').get_by_role('button', name=_('Delete')).click()
[docs]
def modify_text_field(self, name: str | CreatedItem, label: str = _('Description'), value: str = 'description') -> None:
"""
Shortcut method to open the details of an object, fill a field with a value and save
:param name: the name of the object to modify
:param label: the label of the textbox to fill the text into
:param value: the value to fill into the textbox
"""
if isinstance(name, CreatedItem):
name = name.identifying_name
modify_object = self.open_details(name)
modify_object.fill_field(label, value)
modify_object.save()
[docs]
class PortalModule(GenericUDMModule):
def __init__(self, tester: UMCBrowserTest) -> None:
super().__init__(tester, _('Portal'))
[docs]
def add(
self,
name: str = 'portal_name',
lang_code: str = 'English/USA',
display_name: str = 'Portal Display Name',
) -> CreatedItem:
add_object = self.add_object_dialog()
add_object.tester.fill_combobox('Type', 'Portal: Portal')
add_object.next()
dv = DetailsView(self.tester)
dv.fill_field(_('Internal name'), name)
dv.tester.fill_combobox('Language code', lang_code)
dv.fill_field(_('Display Name'), display_name)
dv.save(_('Create Portal'))
return CreatedItem(name)
[docs]
class UserModule(GenericUDMModule):
def __init__(self, tester: UMCBrowserTest) -> None:
super().__init__(tester, _('Users'))
[docs]
def handle_comboboxes(self, add_object: AddObjectDialog, template: str | None) -> None:
combobox_filled = False
# in some cases there might be a dialog with a combobox pop-up where none is expected
# here we make sure that either a detail view or add dialog is displayed
# before checking if a combobox is visible
dialog = add_object.locator.get_by_text(_('Add a new user'))
detail = self.page.get_by_role('heading', name=_('Basic settings'))
expect(dialog.or_(detail)).to_be_visible()
# in case there is a different user container we want to select the default one here
filter = self.page.get_by_label(_('Container'))
container_combobox = self.page.get_by_role('combobox').filter(has=filter)
if container_combobox.is_visible():
add_object.tester.fill_combobox(_('Container'), f'{self.tester.domainname}:/users')
combobox_filled = True
# in case there is a template when none is expected
filter = self.page.get_by_label(_('User template'))
template_combobox = self.page.get_by_role('combobox').filter(has=filter)
if template is None and template_combobox.is_visible():
add_object.tester.fill_combobox(_('User template'), _('None'))
combobox_filled = True
if template is not None:
add_object.tester.fill_combobox(_('User template'), template)
combobox_filled = True
if combobox_filled:
add_object.next()
[docs]
def create_object(
self,
name: str = 'user_name',
first_name: str = 'first_name',
last_name: str = 'last_name',
password: str = 'univention',
template: str | None = None,
) -> CreatedItem:
"""
Add a new user with the given information
:return: CreatedItem which can be passed to subsequent methods of this class to modify the added user
"""
add_object = self.add_object_dialog()
self.handle_comboboxes(add_object, template)
add_object.fill_field(_('First name'), first_name)
add_object.fill_field(_('Last name'), last_name)
add_object.fill_field(_('User name'), name)
add_object.next()
add_object.fill_field(f"{_('Password')} *", password, exact=True)
add_object.fill_field(f"{_('Password (retype)')} *", password, exact=True)
add_object.finish('Create User')
if ucr.is_true('ad/member'):
return CreatedItem(first_name + ' ' + last_name)
return CreatedItem(name)
[docs]
def copy_user(self, original_name: str, name: str, last_name: str = 'last_name', password: str = 'univention') -> None:
self.tester.check_checkbox_in_grid_by_name(original_name)
self.page.get_by_role('button', name=_('more')).click()
self.page.get_by_role('cell', name=_('copy')).click()
add_object = AddObjectDialog(self.tester, self.page.get_by_role('dialog'))
self.handle_comboboxes(add_object, None)
detail_view = DetailsView(self.tester)
detail_view.fill_field(f"{_('Last name')} *", last_name)
detail_view.fill_field(f"{_('User name')} *", name)
detail_view.fill_field(f"{_('Password')} *", password)
detail_view.fill_field(f"{_('Password (retype)')} *", password)
detail_view.save(_('Create User'))
[docs]
class GroupModule(GenericUDMModule):
def __init__(self, tester: UMCBrowserTest) -> None:
super().__init__(tester, _('Groups'))
[docs]
def create_object(self, group_name: str = 'group_name') -> CreatedItem:
"""
Add a new group with the given information
:return: CreatedItem which can be passed to subsequent methods of this class to modify the added group
"""
detail_view = self.add_object_detail_view()
detail_view.fill_field('name', group_name)
detail_view.save(_('Create Group'))
expect(self.page.get_by_role('gridcell').filter(has_text=group_name).first, 'expect created group to be visible in grid').to_be_visible()
return CreatedItem(group_name)
[docs]
class PoliciesModule(GenericUDMModule):
def __init__(self, tester: UMCBrowserTest) -> None:
super().__init__(tester, _('Policies'))
[docs]
def create_object(self, policy_name: str = 'policy_name') -> CreatedItem:
"""
Add a new policy with the given information
:return: CreatedItem which can be passed to subsequent methods of this class to modify the added policy
"""
add_dialog = self.add_object_dialog()
add_dialog.next()
dv = DetailsView(self.tester)
dv.fill_field(f"{_('Name')} *", policy_name)
dv.save(_('Create Policy'))
expect(self.page.get_by_role('gridcell').filter(has_text=policy_name).first, 'expect created group to be visible in grid').to_be_visible()
return CreatedItem(policy_name)
[docs]
def modify_text_field(self, created_item: str | CreatedItem, label: str = _('Update to this UCS version'), value: str = '4.0') -> None:
super().modify_text_field(created_item, label, value)
[docs]
class ComputerModule(GenericUDMModule):
def __init__(self, tester: UMCBrowserTest) -> None:
super().__init__(tester, _('Computers'))
[docs]
def create_object(self, computer_name: str = 'computer_name_8') -> CreatedItem:
"""
Add a new computer with the given information
:return: CreatedItem which can be passed to subsequent methods of this class to modify the added computer
"""
add_dialog = self.add_object_dialog()
add_dialog.next()
add_dialog.fill_field(f"{_('Windows workstation/server name')} *", computer_name)
add_dialog.finish(_('Create Computer'))
return CreatedItem(computer_name)