|
from __future__ import annotations
|
|
|
|
import time
|
|
|
|
|
|
def idle_keepalive_pause(page, total_seconds: int, chunk_seconds: int = 5) -> None:
|
|
"""
|
|
Wait out a required cooldown (the 15-PDF pause, the rate-limit backoff,
|
|
etc.) WITHOUT letting the browser tab go completely idle.
|
|
|
|
Ported from eap_sez_automation.py. A plain wait_for_timeout(120_000)
|
|
leaves the page with zero activity for two full minutes -- which lines
|
|
up exactly with when the NSDL session was observed expiring mid-run.
|
|
This performs small, harmless mouse-wheel scrolls (down for a few
|
|
seconds, then up, repeating) so the tab registers ordinary user
|
|
activity for the whole cooldown, while still taking the full required
|
|
duration and making no extra network requests (so it can't trip the
|
|
portal's rate limit itself). This is a best-effort measure, not a
|
|
guarantee -- session-expiry detection/recovery should still run
|
|
immediately afterward as the real safety net.
|
|
"""
|
|
elapsed = 0.0
|
|
direction = 1 # 1 = scroll down, -1 = scroll up
|
|
|
|
while elapsed < total_seconds:
|
|
chunk_start = time.time()
|
|
remaining = total_seconds - elapsed
|
|
this_chunk = min(chunk_seconds, remaining)
|
|
|
|
try:
|
|
steps = max(1, round(this_chunk))
|
|
step_delta = (300 * direction) / steps
|
|
step_wait_ms = int((this_chunk / steps) * 1000)
|
|
|
|
for _ in range(steps):
|
|
page.mouse.wheel(0, step_delta)
|
|
page.wait_for_timeout(step_wait_ms)
|
|
except Exception:
|
|
# Page/tab not in a state where scrolling is possible right now
|
|
# -- fall back to a plain wait for this chunk. Session-expiry
|
|
# detection elsewhere still catches an actual timeout.
|
|
time.sleep(this_chunk)
|
|
|
|
direction *= -1
|
|
elapsed += time.time() - chunk_start
|
|
|
|
|
|
def immunize_page_against_popups(context) -> None:
|
|
"""
|
|
Install persistent protection against the NSDL driver.js welcome tour and
|
|
known overlay elements that can block normal portal interaction.
|
|
|
|
A modest polling interval is intentionally used instead of a document-wide
|
|
MutationObserver because NSDL performs frequent ASP.NET/AJAX DOM updates;
|
|
a global observer can create a DOM-mutation feedback loop and freeze the UI.
|
|
"""
|
|
context.add_init_script(
|
|
"""
|
|
window.Driver = function() {
|
|
return {
|
|
start: function() {},
|
|
reset: function() {},
|
|
destroy: function() {},
|
|
defineSteps: function() {},
|
|
showNextStep: function() {},
|
|
preventMove: function() {}
|
|
};
|
|
};
|
|
|
|
const installNsdlPopupProtection = () => {
|
|
const style = document.createElement('style');
|
|
style.innerHTML = `
|
|
.driver-overlay,
|
|
.driver-popover,
|
|
.driver-popover-tip,
|
|
.driver-popover-footer,
|
|
.driver-popover-description,
|
|
.driver-popover-title,
|
|
.driver-stage-no-animation,
|
|
.modal-backdrop,
|
|
.overlay {
|
|
display: none !important;
|
|
visibility: hidden !important;
|
|
pointer-events: none !important;
|
|
width: 0 !important;
|
|
height: 0 !important;
|
|
opacity: 0 !important;
|
|
}
|
|
|
|
html,
|
|
body {
|
|
overflow: auto !important;
|
|
pointer-events: auto !important;
|
|
}
|
|
`;
|
|
|
|
if (document.head) {
|
|
document.head.appendChild(style);
|
|
}
|
|
|
|
const stripOverlayState = () => {
|
|
if (document.body) {
|
|
document.body.classList.remove('driver-active', 'driver-fade');
|
|
document.body.style.overflow = 'auto';
|
|
document.body.style.pointerEvents = 'auto';
|
|
}
|
|
|
|
if (document.documentElement) {
|
|
document.documentElement.classList.remove('driver-active', 'driver-fade');
|
|
document.documentElement.style.overflow = 'auto';
|
|
}
|
|
|
|
document.querySelectorAll(
|
|
'.driver-overlay, .driver-popover, .driver-popover-tip, ' +
|
|
'.driver-popover-footer, .driver-popover-description, ' +
|
|
'.driver-popover-title, .modal-backdrop, .overlay'
|
|
).forEach((el) => el.remove());
|
|
};
|
|
|
|
stripOverlayState();
|
|
setInterval(stripOverlayState, 400);
|
|
};
|
|
|
|
if (document.body) {
|
|
installNsdlPopupProtection();
|
|
} else {
|
|
document.addEventListener('DOMContentLoaded', installNsdlPopupProtection);
|
|
}
|
|
"""
|
|
)
|
|
|
|
|
|
def clean_popups_now(page) -> None:
|
|
"""Remove known popup/overlay elements from the active page immediately."""
|
|
try:
|
|
page.evaluate(
|
|
"""
|
|
document.querySelectorAll(
|
|
'.driver-overlay, .driver-popover, .driver-popover-tip, ' +
|
|
'.driver-popover-footer, .driver-popover-description, ' +
|
|
'.driver-popover-title, .modal-backdrop, .overlay'
|
|
).forEach((el) => el.remove());
|
|
|
|
if (document.body) {
|
|
document.body.classList.remove('driver-active', 'driver-fade');
|
|
document.body.style.overflow = 'auto';
|
|
document.body.style.pointerEvents = 'auto';
|
|
}
|
|
|
|
if (document.documentElement) {
|
|
document.documentElement.classList.remove('driver-active', 'driver-fade');
|
|
document.documentElement.style.overflow = 'auto';
|
|
}
|
|
"""
|
|
)
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
def handle_welcome_popup(page) -> None:
|
|
"""Belt-and-braces runtime close of the NSDL Welcome Tour, then cleanup."""
|
|
try:
|
|
page.wait_for_timeout(500)
|
|
popup = page.locator(".driver-popover")
|
|
close_btn = page.locator(".driver-popover-close-btn")
|
|
|
|
if popup.count() > 0 and popup.is_visible():
|
|
try:
|
|
close_btn.first.evaluate("(el) => el.click()")
|
|
except Exception:
|
|
try:
|
|
close_btn.first.click(timeout=3000, force=True)
|
|
except Exception:
|
|
pass
|
|
|
|
try:
|
|
popup.wait_for(state="hidden", timeout=5000)
|
|
except Exception:
|
|
pass
|
|
except Exception:
|
|
pass
|
|
|
|
clean_popups_now(page)
|
|
|
|
|
|
def safe_click(page, locator_or_selector) -> None:
|
|
"""
|
|
Reliable NSDL click wrapper.
|
|
|
|
The popup layer is cleaned immediately before the click. A forced click is
|
|
attempted first, followed by a normal Playwright click as a second attempt.
|
|
"""
|
|
clean_popups_now(page)
|
|
|
|
locator = locator_or_selector if hasattr(locator_or_selector, "click") else page.locator(locator_or_selector)
|
|
|
|
try:
|
|
locator.scroll_into_view_if_needed(timeout=5000)
|
|
except Exception:
|
|
pass
|
|
|
|
clean_popups_now(page)
|
|
|
|
try:
|
|
locator.click(timeout=10000, force=True)
|
|
return
|
|
except Exception as first_exc:
|
|
clean_popups_now(page)
|
|
try:
|
|
locator.click(timeout=10000)
|
|
return
|
|
except Exception as second_exc:
|
|
raise RuntimeError(f"Could not click NSDL portal control: {second_exc}") from first_exc
|
|
|
|
|
|
def enter_masked_date(page, locator, value: str) -> None:
|
|
"""Set an NSDL masked date field using the portal's expected interaction."""
|
|
clean_popups_now(page)
|
|
safe_click(page, locator)
|
|
|
|
try:
|
|
locator.evaluate("(el) => el.removeAttribute('readonly')")
|
|
except Exception:
|
|
pass
|
|
|
|
locator.evaluate("(el) => el.value = ''")
|
|
if value:
|
|
locator.type(value, delay=50)
|
|
|
|
clean_popups_now(page)
|