Fix installer selectors for setupSuperUser UI variants
This commit is contained in:
@@ -61,6 +61,56 @@ NEXT_BUTTON_CANDIDATES: list[tuple[str, str]] = [
|
|||||||
("button", "Fortfahren"),
|
("button", "Fortfahren"),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
CONTINUE_TO_MATOMO_CANDIDATES: list[tuple[str, str]] = [
|
||||||
|
("button", "Continue to Matomo »"),
|
||||||
|
("button", "Continue to Matomo"),
|
||||||
|
("link", "Continue to Matomo »"),
|
||||||
|
("link", "Continue to Matomo"),
|
||||||
|
]
|
||||||
|
|
||||||
|
SUPERUSER_LOGIN_SELECTORS = (
|
||||||
|
"#login-0",
|
||||||
|
"#login",
|
||||||
|
"input[name='login']",
|
||||||
|
"form#generalsetupform input[name='login']",
|
||||||
|
)
|
||||||
|
SUPERUSER_PASSWORD_SELECTORS = (
|
||||||
|
"#password-0",
|
||||||
|
"#password",
|
||||||
|
"input[name='password']",
|
||||||
|
"form#generalsetupform input[name='password']",
|
||||||
|
)
|
||||||
|
SUPERUSER_PASSWORD_REPEAT_SELECTORS = (
|
||||||
|
"#password_bis-0",
|
||||||
|
"#password_bis",
|
||||||
|
"input[name='password_bis']",
|
||||||
|
"form#generalsetupform input[name='password_bis']",
|
||||||
|
)
|
||||||
|
SUPERUSER_EMAIL_SELECTORS = (
|
||||||
|
"#email-0",
|
||||||
|
"#email",
|
||||||
|
"input[name='email']",
|
||||||
|
"form#generalsetupform input[name='email']",
|
||||||
|
)
|
||||||
|
SUPERUSER_SUBMIT_SELECTORS = (
|
||||||
|
"#submit-0",
|
||||||
|
"#submit",
|
||||||
|
"form#generalsetupform button[type='submit']",
|
||||||
|
"form#generalsetupform input[type='submit']",
|
||||||
|
)
|
||||||
|
FIRST_WEBSITE_NAME_SELECTORS = (
|
||||||
|
"#siteName-0",
|
||||||
|
"#siteName",
|
||||||
|
"input[name='siteName']",
|
||||||
|
"form#websitesetupform input[name='siteName']",
|
||||||
|
)
|
||||||
|
FIRST_WEBSITE_URL_SELECTORS = (
|
||||||
|
"#url-0",
|
||||||
|
"#url",
|
||||||
|
"input[name='url']",
|
||||||
|
"form#websitesetupform input[name='url']",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _log(msg: str) -> None:
|
def _log(msg: str) -> None:
|
||||||
# IMPORTANT: logs must not pollute stdout (tests expect only token on stdout)
|
# IMPORTANT: logs must not pollute stdout (tests expect only token on stdout)
|
||||||
@@ -286,11 +336,86 @@ def _first_next_locator(page):
|
|||||||
return None, ""
|
return None, ""
|
||||||
|
|
||||||
|
|
||||||
|
def _first_present_css_locator(page, selectors, *, timeout_s: float = 0.2):
|
||||||
|
for selector in selectors:
|
||||||
|
loc = page.locator(selector)
|
||||||
|
try:
|
||||||
|
if _count_locator(loc, timeout_s=timeout_s) > 0:
|
||||||
|
return loc.first, f"css:{selector}"
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
return None, ""
|
||||||
|
|
||||||
|
|
||||||
|
def _first_continue_to_matomo_locator(page, *, timeout_s: float = 0.2):
|
||||||
|
for role, name in CONTINUE_TO_MATOMO_CANDIDATES:
|
||||||
|
loc = page.get_by_role(role, name=name)
|
||||||
|
try:
|
||||||
|
if _count_locator(loc, timeout_s=timeout_s) > 0 and loc.first.is_visible():
|
||||||
|
return loc.first, f"{role}:{name}"
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
text_loc = page.get_by_text("Continue to Matomo", exact=False)
|
||||||
|
try:
|
||||||
|
if _count_locator(text_loc, timeout_s=timeout_s) > 0 and text_loc.first.is_visible():
|
||||||
|
return text_loc.first, "text:Continue to Matomo*"
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return None, ""
|
||||||
|
|
||||||
|
|
||||||
|
def _has_superuser_login_field(page, *, timeout_s: float = 0.2) -> bool:
|
||||||
|
loc, _ = _first_present_css_locator(
|
||||||
|
page, SUPERUSER_LOGIN_SELECTORS, timeout_s=timeout_s
|
||||||
|
)
|
||||||
|
return loc is not None
|
||||||
|
|
||||||
|
|
||||||
|
def _has_first_website_name_field(page, *, timeout_s: float = 0.2) -> bool:
|
||||||
|
loc, _ = _first_present_css_locator(
|
||||||
|
page, FIRST_WEBSITE_NAME_SELECTORS, timeout_s=timeout_s
|
||||||
|
)
|
||||||
|
return loc is not None
|
||||||
|
|
||||||
|
|
||||||
|
def _has_continue_to_matomo_action(page, *, timeout_s: float = 0.2) -> bool:
|
||||||
|
loc, _ = _first_continue_to_matomo_locator(page, timeout_s=timeout_s)
|
||||||
|
return loc is not None
|
||||||
|
|
||||||
|
|
||||||
|
def _fill_required_input(page, selectors, value: str, *, label: str) -> None:
|
||||||
|
loc, _ = _first_present_css_locator(page, selectors, timeout_s=1.0)
|
||||||
|
if loc is None:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Could not locate required installer field '{label}' "
|
||||||
|
f"(url={page.url}, step={_get_step_hint(page.url)})."
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
loc.click(timeout=2_000)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
loc.fill(value)
|
||||||
|
|
||||||
|
|
||||||
|
def _fill_optional_input(page, selectors, value: str) -> bool:
|
||||||
|
loc, _ = _first_present_css_locator(page, selectors, timeout_s=0.5)
|
||||||
|
if loc is None:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
loc.click(timeout=2_000)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
loc.fill(value)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def _installer_interactive(page) -> bool:
|
def _installer_interactive(page) -> bool:
|
||||||
checks = [
|
checks = [
|
||||||
_count_locator(page.locator("#login-0")) > 0,
|
_has_superuser_login_field(page),
|
||||||
_count_locator(page.locator("#siteName-0")) > 0,
|
_has_first_website_name_field(page),
|
||||||
_count_locator(page.get_by_role("button", name="Continue to Matomo »")) > 0,
|
_has_continue_to_matomo_action(page),
|
||||||
]
|
]
|
||||||
loc, _ = _first_next_locator(page)
|
loc, _ = _first_next_locator(page)
|
||||||
return any(checks) or loc is not None
|
return any(checks) or loc is not None
|
||||||
@@ -347,24 +472,19 @@ def _click_next_with_wait(page, *, timeout_s: int) -> str:
|
|||||||
|
|
||||||
# Some installer transitions render the next form asynchronously without
|
# Some installer transitions render the next form asynchronously without
|
||||||
# exposing another "Next" control yet. Treat this as progress.
|
# exposing another "Next" control yet. Treat this as progress.
|
||||||
if _count_locator(page.locator("#login-0"), timeout_s=0.2) > 0:
|
if _has_superuser_login_field(page, timeout_s=0.2):
|
||||||
_log(
|
_log(
|
||||||
"[install] Superuser form became available without explicit click; "
|
"[install] Superuser form became available without explicit click; "
|
||||||
f"staying on step {current_step} (url {current_url})"
|
f"staying on step {current_step} (url {current_url})"
|
||||||
)
|
)
|
||||||
return current_step
|
return current_step
|
||||||
if _count_locator(page.locator("#siteName-0"), timeout_s=0.2) > 0:
|
if _has_first_website_name_field(page, timeout_s=0.2):
|
||||||
_log(
|
_log(
|
||||||
"[install] First website form became available without explicit click; "
|
"[install] First website form became available without explicit click; "
|
||||||
f"staying on step {current_step} (url {current_url})"
|
f"staying on step {current_step} (url {current_url})"
|
||||||
)
|
)
|
||||||
return current_step
|
return current_step
|
||||||
if (
|
if _has_continue_to_matomo_action(page, timeout_s=0.2):
|
||||||
_count_locator(
|
|
||||||
page.get_by_role("button", name="Continue to Matomo »"), timeout_s=0.2
|
|
||||||
)
|
|
||||||
> 0
|
|
||||||
):
|
|
||||||
_log(
|
_log(
|
||||||
"[install] Continue-to-Matomo action is available without explicit click; "
|
"[install] Continue-to-Matomo action is available without explicit click; "
|
||||||
f"staying on step {current_step} (url {current_url})"
|
f"staying on step {current_step} (url {current_url})"
|
||||||
@@ -612,7 +732,7 @@ class WebInstaller(Installer):
|
|||||||
|
|
||||||
progress_deadline = time.time() + INSTALLER_STEP_DEADLINE_S
|
progress_deadline = time.time() + INSTALLER_STEP_DEADLINE_S
|
||||||
|
|
||||||
while _count_locator(page.locator("#login-0")) == 0:
|
while not _has_superuser_login_field(page):
|
||||||
if time.time() >= progress_deadline:
|
if time.time() >= progress_deadline:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
"Installer did not reach superuser step "
|
"Installer did not reach superuser step "
|
||||||
@@ -632,18 +752,27 @@ class WebInstaller(Installer):
|
|||||||
_click_next_with_wait(page, timeout_s=step_timeout)
|
_click_next_with_wait(page, timeout_s=step_timeout)
|
||||||
_page_warnings(page)
|
_page_warnings(page)
|
||||||
|
|
||||||
page.locator("#login-0").click()
|
_fill_required_input(
|
||||||
page.locator("#login-0").fill(config.admin_user)
|
page,
|
||||||
|
SUPERUSER_LOGIN_SELECTORS,
|
||||||
page.locator("#password-0").click()
|
config.admin_user,
|
||||||
page.locator("#password-0").fill(config.admin_password)
|
label="superuser login",
|
||||||
|
)
|
||||||
if _count_locator(page.locator("#password_bis-0")) > 0:
|
_fill_required_input(
|
||||||
page.locator("#password_bis-0").click()
|
page,
|
||||||
page.locator("#password_bis-0").fill(config.admin_password)
|
SUPERUSER_PASSWORD_SELECTORS,
|
||||||
|
config.admin_password,
|
||||||
page.locator("#email-0").click()
|
label="superuser password",
|
||||||
page.locator("#email-0").fill(config.admin_email)
|
)
|
||||||
|
_fill_optional_input(
|
||||||
|
page, SUPERUSER_PASSWORD_REPEAT_SELECTORS, config.admin_password
|
||||||
|
)
|
||||||
|
_fill_required_input(
|
||||||
|
page,
|
||||||
|
SUPERUSER_EMAIL_SELECTORS,
|
||||||
|
config.admin_email,
|
||||||
|
label="superuser email",
|
||||||
|
)
|
||||||
_page_warnings(page)
|
_page_warnings(page)
|
||||||
|
|
||||||
submitted_superuser = False
|
submitted_superuser = False
|
||||||
@@ -689,20 +818,27 @@ class WebInstaller(Installer):
|
|||||||
if submitted_superuser:
|
if submitted_superuser:
|
||||||
_wait_dom_settled(page)
|
_wait_dom_settled(page)
|
||||||
_log("[install] Submitted superuser form via form.requestSubmit().")
|
_log("[install] Submitted superuser form via form.requestSubmit().")
|
||||||
elif _count_locator(page.locator("#submit-0")) > 0:
|
|
||||||
page.locator("#submit-0").click(timeout=2_000)
|
|
||||||
_wait_dom_settled(page)
|
|
||||||
_log("[install] Submitted superuser form via #submit-0 fallback.")
|
|
||||||
else:
|
else:
|
||||||
_click_next_with_wait(page, timeout_s=INSTALLER_STEP_TIMEOUT_S)
|
submit_loc, submit_label = _first_present_css_locator(
|
||||||
|
page, SUPERUSER_SUBMIT_SELECTORS, timeout_s=0.5
|
||||||
|
)
|
||||||
|
if submit_loc is not None:
|
||||||
|
submit_loc.click(timeout=2_000)
|
||||||
|
_wait_dom_settled(page)
|
||||||
|
_log(
|
||||||
|
"[install] Submitted superuser form via "
|
||||||
|
f"{submit_label} fallback."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
_click_next_with_wait(page, timeout_s=INSTALLER_STEP_TIMEOUT_S)
|
||||||
|
|
||||||
superuser_progress_deadline = time.time() + INSTALLER_STEP_TIMEOUT_S
|
superuser_progress_deadline = time.time() + INSTALLER_STEP_TIMEOUT_S
|
||||||
while time.time() < superuser_progress_deadline:
|
while time.time() < superuser_progress_deadline:
|
||||||
_wait_dom_settled(page)
|
_wait_dom_settled(page)
|
||||||
if _count_locator(page.locator("#login-0")) == 0:
|
if not _has_superuser_login_field(page):
|
||||||
break
|
break
|
||||||
page.wait_for_timeout(300)
|
page.wait_for_timeout(300)
|
||||||
if _count_locator(page.locator("#login-0")) > 0:
|
if _has_superuser_login_field(page):
|
||||||
_page_warnings(page)
|
_page_warnings(page)
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
"Superuser form submit did not progress to first website setup "
|
"Superuser form submit did not progress to first website setup "
|
||||||
@@ -773,13 +909,10 @@ class WebInstaller(Installer):
|
|||||||
"[install] Submitted first website form via form.requestSubmit()."
|
"[install] Submitted first website form via form.requestSubmit()."
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
if _count_locator(page.locator("#siteName-0")) > 0:
|
_fill_optional_input(
|
||||||
page.locator("#siteName-0").click()
|
page, FIRST_WEBSITE_NAME_SELECTORS, DEFAULT_SITE_NAME
|
||||||
page.locator("#siteName-0").fill(DEFAULT_SITE_NAME)
|
)
|
||||||
|
_fill_optional_input(page, FIRST_WEBSITE_URL_SELECTORS, DEFAULT_SITE_URL)
|
||||||
if _count_locator(page.locator("#url-0")) > 0:
|
|
||||||
page.locator("#url-0").click()
|
|
||||||
page.locator("#url-0").fill(DEFAULT_SITE_URL)
|
|
||||||
|
|
||||||
_page_warnings(page)
|
_page_warnings(page)
|
||||||
|
|
||||||
@@ -810,10 +943,10 @@ class WebInstaller(Installer):
|
|||||||
first_website_progress_deadline = time.time() + INSTALLER_STEP_TIMEOUT_S
|
first_website_progress_deadline = time.time() + INSTALLER_STEP_TIMEOUT_S
|
||||||
while time.time() < first_website_progress_deadline:
|
while time.time() < first_website_progress_deadline:
|
||||||
_wait_dom_settled(page)
|
_wait_dom_settled(page)
|
||||||
if _count_locator(page.locator("#siteName-0")) == 0:
|
if not _has_first_website_name_field(page):
|
||||||
break
|
break
|
||||||
page.wait_for_timeout(300)
|
page.wait_for_timeout(300)
|
||||||
if _count_locator(page.locator("#siteName-0")) > 0:
|
if _has_first_website_name_field(page):
|
||||||
_page_warnings(page)
|
_page_warnings(page)
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
"First website form submit did not progress to tracking code "
|
"First website form submit did not progress to tracking code "
|
||||||
@@ -828,13 +961,9 @@ class WebInstaller(Installer):
|
|||||||
_wait_dom_settled(page)
|
_wait_dom_settled(page)
|
||||||
_page_warnings(page)
|
_page_warnings(page)
|
||||||
|
|
||||||
if (
|
continue_loc, _ = _first_continue_to_matomo_locator(page)
|
||||||
_count_locator(
|
if continue_loc is not None:
|
||||||
page.get_by_role("button", name="Continue to Matomo »")
|
continue_loc.click()
|
||||||
)
|
|
||||||
> 0
|
|
||||||
):
|
|
||||||
page.get_by_role("button", name="Continue to Matomo »").click()
|
|
||||||
_wait_dom_settled(page)
|
_wait_dom_settled(page)
|
||||||
_page_warnings(page)
|
_page_warnings(page)
|
||||||
|
|
||||||
|
|||||||
@@ -51,6 +51,26 @@ class _RoleLocator:
|
|||||||
return self._count_value > 0
|
return self._count_value > 0
|
||||||
|
|
||||||
|
|
||||||
|
class _NameOnlyStaticLocator:
|
||||||
|
def __init__(self, page, selector: str):
|
||||||
|
self._page = page
|
||||||
|
self._selector = selector
|
||||||
|
|
||||||
|
def count(self) -> int:
|
||||||
|
if self._selector == "input[name='login']":
|
||||||
|
return 1 if self._page.login_visible else 0
|
||||||
|
if self._selector == "input[name='siteName']":
|
||||||
|
return 0
|
||||||
|
return 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def first(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def is_visible(self) -> bool:
|
||||||
|
return self.count() > 0
|
||||||
|
|
||||||
|
|
||||||
class _NoNextButLoginAppearsPage:
|
class _NoNextButLoginAppearsPage:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.url = "http://matomo/index.php?action=setupSuperUser&module=Installation"
|
self.url = "http://matomo/index.php?action=setupSuperUser&module=Installation"
|
||||||
@@ -78,6 +98,33 @@ class _NoNextButLoginAppearsPage:
|
|||||||
self.login_visible = True
|
self.login_visible = True
|
||||||
|
|
||||||
|
|
||||||
|
class _NoNextButNamedLoginAppearsPage:
|
||||||
|
def __init__(self):
|
||||||
|
self.url = "http://matomo/index.php?action=setupSuperUser&module=Installation"
|
||||||
|
self.login_visible = False
|
||||||
|
self._wait_calls = 0
|
||||||
|
|
||||||
|
def locator(self, selector: str):
|
||||||
|
return _NameOnlyStaticLocator(self, selector)
|
||||||
|
|
||||||
|
def get_by_role(self, role: str, name: str):
|
||||||
|
return _RoleLocator(0)
|
||||||
|
|
||||||
|
def get_by_text(self, *_args, **_kwargs):
|
||||||
|
return _RoleLocator(0)
|
||||||
|
|
||||||
|
def title(self) -> str:
|
||||||
|
return "setupSuperUser"
|
||||||
|
|
||||||
|
def wait_for_load_state(self, *_args, **_kwargs):
|
||||||
|
return None
|
||||||
|
|
||||||
|
def wait_for_timeout(self, *_args, **_kwargs):
|
||||||
|
self._wait_calls += 1
|
||||||
|
if self._wait_calls >= 1:
|
||||||
|
self.login_visible = True
|
||||||
|
|
||||||
|
|
||||||
class TestWebInstallerLocatorCountIntegration(unittest.TestCase):
|
class TestWebInstallerLocatorCountIntegration(unittest.TestCase):
|
||||||
def test_retries_transient_navigation_error(self) -> None:
|
def test_retries_transient_navigation_error(self) -> None:
|
||||||
locator = _FlakyLocator(
|
locator = _FlakyLocator(
|
||||||
@@ -113,6 +160,14 @@ class TestWebInstallerLocatorCountIntegration(unittest.TestCase):
|
|||||||
self.assertEqual(step, "Installation:setupSuperUser")
|
self.assertEqual(step, "Installation:setupSuperUser")
|
||||||
self.assertTrue(page.login_visible)
|
self.assertTrue(page.login_visible)
|
||||||
|
|
||||||
|
def test_click_next_wait_treats_named_login_form_as_progress(self) -> None:
|
||||||
|
page = _NoNextButNamedLoginAppearsPage()
|
||||||
|
|
||||||
|
step = _click_next_with_wait(page, timeout_s=1)
|
||||||
|
|
||||||
|
self.assertEqual(step, "Installation:setupSuperUser")
|
||||||
|
self.assertTrue(page.login_visible)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
Reference in New Issue
Block a user