"""Playwright implementation of the existing Angular setup UI.""" import re import time from urllib.parse import urlparse from playwright.sync_api import expect from tests.locators import Locator class SetupPage: def __init__(self, page, folder): self.page = page self.folder = folder self.screenshots = [] self.dialogs = [] self.token_id = None page.on("dialog", self._dialog) def _dialog(self, dialog): self.dialogs.append(dialog.message) dialog.accept() def visible(self, xpath): return self.page.locator("xpath=" + xpath).filter(visible=True) def capture(self, name): path = self.folder / f"{len(self.screenshots) + 1:02d}_{name}.png" self.page.screenshot(path=str(path)) self.screenshots.append((name, path)) print(f"STEP {len(self.screenshots)}: {name}") def click(self, xpath, name): self.visible(xpath).click() self.capture(name) def fill(self, xpath, value): field = self.visible(xpath) field.fill(value) field.press("Tab") expect(field).to_have_value(value) def close_notice(self): # Only the known TAC-expiry notice; never dismiss arbitrary validation dialogs. notice = self.page.locator(".modal:visible").filter(has_text=re.compile(r"TAC.*expir", re.I | re.S)) if notice.count() == 1: notice.locator("button.close").click() def login(self, config, timeout): self.page.goto(config["url"]) self.visible(Locator.userid).fill(config["login"]["username"]) self.visible(Locator.password).fill(config["login"]["password"]) captcha_length = int(config.get("playwright", {}).get("captcha_length", 6)) if captcha_length < 1: raise ValueError("playwright.captcha_length must be positive.") print(f"Credentials filled. Enter the {captcha_length}-character CAPTCHA in the browser; " f"Sign in will be clicked automatically. Waiting up to {timeout // 1000} seconds...", flush=True) captcha = self.page.locator('input[name="captcha"][ng-model="login.captcha"]') expect(captcha).to_have_value(re.compile(r"^\S{" + str(captcha_length) + r"}$"), timeout=timeout) self.visible(Locator.submitbtn).click() expect(self.visible(Locator.dashboard_marker)).to_be_visible(timeout=30_000) self.close_notice() self.capture("login_success") def menu(self, xpath): self.close_notice() if not self.visible(xpath).count(): self.visible(Locator.tradingmaintenance).click() self.visible(xpath).click() def choose(self, toggle, value): expect(toggle).to_be_visible() if toggle.inner_text().strip() == value: return toggle.click() search = self.page.locator("input.ui-select-search:visible") expect(search).to_have_count(1) search.fill(value) if toggle.is_visible() and toggle.inner_text().strip() == value: return option = self.page.locator(".ui-select-choices-row:visible").filter( has_text=re.compile(r"^\s*" + re.escape(value) + r"\s*$") ) option.click() expect(self.page.locator(".ui-select-choices-row:visible")).to_have_count(0) def grid_button(self, label, row=None): scope = self.page if row is not None: aliases = {"ORDER_NOR": ("ORDER_NOR", "ORDER_NORMAL"), "ORDER_EXCH": ("ORDER_EXCH", "ORDER_EXCHANGE")} labels = aliases.get(row, (row,)) pattern = re.compile(r"^\s*(?:" + "|".join(re.escape(label) for label in labels) + r")\s*$") scope = self.page.locator(".ui-grid-row:visible").filter(has=self.page.get_by_text(pattern)) button = scope.get_by_role("button", name=label, exact=True) # Fail on multiple matches instead of silently editing a different token. expect(button).to_have_count(1, timeout=30_000) button.click() def save(self, xpath, name, timeout=30): """Require an explicit success message; a click alone is not a pass.""" button = self.visible(xpath) if xpath in (Locator.savetokenbtn, Locator.savetimesessionbtn, Locator.savetokenandactivatebtn): endpoint = {Locator.savetokenbtn: "saveTokenDetails", Locator.savetimesessionbtn: "updateTimeSessionMap", Locator.savetokenandactivatebtn: "updateTokenMstr"}[xpath] with self.page.expect_response( lambda response: response.request.method == "POST" and urlparse(response.url).path.endswith("/tokenCtrl/" + endpoint), timeout=timeout * 1000, ) as pending: button.click() response = pending.value assert response.ok, f"{name}: server returned HTTP {response.status}" result = response.json() message = result.get("message") or "" if xpath == Locator.savetokenandactivatebtn: assert message.startswith("1|"), ( f"Activation not confirmed for token {self.token_id}: updateTokenMstr " f"returned {result}. The application requires a '1|' success message. " "The token and time sessions are already saved; inspect activation before rerunning." ) # The application hides this button after a successful update; it # does not refresh the Token Master grid at this point. expect(button).to_have_count(0, timeout=30_000) else: assert message.startswith("1|"), f"{name}: {message or 'Missing success response'}" if xpath == Locator.savetokenbtn: match = re.search(r"token Id\s*:\s*([^|\s]+)", message, re.I) assert match, "Save response did not identify the generated token" self.token_id = match.group(1) self.capture(name) return previous_dialogs = len(self.dialogs) success = self.page.locator(".alert-success:visible, .toast-success:visible, [role='alert']:visible").filter( has_text=re.compile(r"success|saved|activated|updated", re.I) ) # An earlier notification must disappear before the next save can be verified. expect(success).to_have_count(0, timeout=30_000) button.click() deadline = time.monotonic() + timeout while time.monotonic() < deadline: messages = self.dialogs[previous_dialogs:] errors = self.page.locator(".alert-danger:visible, .toast-error:visible") if errors.count(): raise AssertionError(f"{name}: {'; '.join(errors.all_text_contents())}") if any(re.search(r"fail|error|invalid|unable|already exists", text, re.I) for text in messages): raise AssertionError(f"{name}: {'; '.join(messages)}") if success.count() or any(re.search(r"success|saved|activated|updated", text, re.I) for text in messages): self.capture(name) return # Poll business confirmation while servicing Playwright dialog events. self.page.wait_for_timeout(100) raise AssertionError(f"{name}: no explicit success confirmation within {timeout}s. " "Inspect the trace for the application's response/notification selector.") def create_session(self, config, window): self.menu(Locator.tradingsession) self.click(Locator.addsessionbtn, "add_session") self.choose(self.page.locator('[aria-label="meInstanceId activate"]'), config["product"]["me_instance"]) self.fill(Locator.inputstartdate, window[0]) self.fill(Locator.inputenddate, window[1]) self.capture("session_values") self.save(Locator.sessionsavebtn, "session_saved") def create_token(self, token): code = token["product_code"] self.menu(Locator.tokengeneration) self.choose(self.page.locator(f"#{Locator.tokenmarketcode} .ui-select-toggle"), token["market_code"]) self.choose(self.visible("//label[normalize-space()='PRODUCT CODE']/following::span[contains(@class,'ui-select-toggle')][1]"), code) self.click(Locator.addnewtokenbtn, "add_token") self.fill(Locator.tokendeliverystartdate, token["delivery_date"]) self.fill(Locator.tokendeliveryenddate, token["delivery_date"]) self.fill(Locator.tokenexpirydate, token["expiry"]) self.save(Locator.savetokenbtn, "token_saved") self.visible(Locator.tokentimesessionbtn).click() self.grid_button("Time Session Details") self.edit_and_activate_token(token) print(f"Created and activated token for {code}") def resume_token(self, token, token_id): self.token_id = token_id self.menu("//a[@href='#/tradingMaintenance/timeSessionView']") for element_id, value in (("mmMarketCode", token["market_code"]), ("mmProductType", token["product_type"]), ("mmProductCode", token["product_code"])): self.choose(self.page.locator(f"#{element_id} .ui-select-toggle"), value) self.visible("//button[@ng-click='getTokenDetailsList()' or @data-ng-click='getTokenDetailsList()']").click() self.grid_button("Time Session Details", token_id) self.capture("resume_saved_token") self.edit_and_activate_token(token) print(f"Updated and activated existing token {token_id}") def edit_and_activate_token(self, token): for name, start, end in token["rows"]: self.grid_button("Details", name) self.visible(Locator.edittimesessionbtn).click() self.fill(Locator.timesessionstartdate, token["session_date"]) self.fill(Locator.timesessionenddate, token["session_date"]) self.fill(Locator.timesessionstarttime, start) self.fill(Locator.timesessionendtime, end) self.save(Locator.savetimesessionbtn, f"{name.lower()}_saved") self.grid_button("Token Details") self.save(Locator.savetokenandactivatebtn, "token_activated") def logout(self): self.visible(Locator.usermenutoggle).click() self.visible(Locator.logoutlink).click() expect(self.visible(Locator.userid)).to_be_visible() self.capture("logout")