210 lines
10 KiB
Python
210 lines
10 KiB
Python
"""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")
|