Initial commit: MISF VOB download and upload automation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
commit
c2dec96b1f
43 files changed
+10944
No files matched your search
@@ -0,0 +1,54 @@
|
||||
"""Tests for multi-attachment URL collection."""
|
||||
from __future__ import annotations
|
||||
|
||||
from onedrive_planner import (
|
||||
_attachment_tab_count,
|
||||
_merge_attachment_hrefs,
|
||||
_should_mark_task_in_progress,
|
||||
)
|
||||
|
||||
SAMPLE_A = (
|
||||
"https://netorg393890.sharepoint.com/sites/ArborHills/Shared%20Documents/"
|
||||
"RICHARDSON%20ARBRHLS%205.26.26.pdf"
|
||||
)
|
||||
SAMPLE_B = (
|
||||
"https://netorg393890.sharepoint.com/sites/ArborHills/Shared%20Documents/"
|
||||
"RICHARDSON%20ARBRHLS%206.4.26.pdf"
|
||||
)
|
||||
|
||||
|
||||
class TestAttachmentMulti:
|
||||
def test_merge_keeps_distinct_pdf_urls(self):
|
||||
merged = _merge_attachment_hrefs(
|
||||
[SAMPLE_A],
|
||||
[SAMPLE_B],
|
||||
[SAMPLE_A],
|
||||
)
|
||||
assert len(merged) == 2
|
||||
assert SAMPLE_A in merged
|
||||
assert SAMPLE_B in merged
|
||||
|
||||
def test_merge_dedupes_same_filename(self):
|
||||
merged = _merge_attachment_hrefs([SAMPLE_A], [SAMPLE_A + "?download=1"])
|
||||
assert len(merged) == 1
|
||||
|
||||
def test_mark_status_only_after_all_files(self, tmp_path):
|
||||
one = tmp_path / "a.pdf"
|
||||
two = tmp_path / "b.pdf"
|
||||
one.write_bytes(b"x")
|
||||
two.write_bytes(b"y")
|
||||
assert not _should_mark_task_in_progress([one], 2)
|
||||
assert _should_mark_task_in_progress([one, two], 2)
|
||||
assert not _should_mark_task_in_progress([], 2)
|
||||
assert _should_mark_task_in_progress([one], 0)
|
||||
|
||||
def test_attachment_tab_count_parses_number(self):
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
tab = MagicMock()
|
||||
tab.is_displayed.return_value = True
|
||||
tab.text = "Attachments (2)"
|
||||
tab.get_attribute.return_value = ""
|
||||
driver = MagicMock()
|
||||
driver.find_elements.return_value = [tab]
|
||||
assert _attachment_tab_count(driver) == 2
|
||||
@@ -0,0 +1,100 @@
|
||||
"""Unit tests for SharePoint URL extraction from attachment rows."""
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import base64
|
||||
|
||||
from onedrive_planner import (
|
||||
_attachment_filename_key,
|
||||
_direct_download_url,
|
||||
_download_href_via_browser_fetch,
|
||||
_extract_sharepoint_url,
|
||||
_is_pdf_bytes,
|
||||
_resolve_attachment_click_target,
|
||||
_sharepoint_download_candidates,
|
||||
_url_from_attachment_element,
|
||||
_viewer_preview_url,
|
||||
)
|
||||
|
||||
SAMPLE = (
|
||||
"https://netorg393890.sharepoint.com/sites/AnberryNursingandRehabilitationCenter/"
|
||||
"Shared%20Documents/ANBERRY%20FS%205.29.26.pdf"
|
||||
)
|
||||
|
||||
|
||||
class TestAttachmentUrl:
|
||||
def test_extract_from_aria_label(self):
|
||||
assert _extract_sharepoint_url(SAMPLE) == SAMPLE
|
||||
|
||||
def test_direct_download_transform(self):
|
||||
dl = _direct_download_url(SAMPLE)
|
||||
assert "download=1" in dl
|
||||
|
||||
def test_viewer_preview_url(self):
|
||||
viewer = _viewer_preview_url(SAMPLE + "?web=1")
|
||||
assert "web=1" in viewer
|
||||
assert "download=1" not in viewer
|
||||
viewer2 = _viewer_preview_url(SAMPLE)
|
||||
assert "web=1" in viewer2
|
||||
|
||||
def test_url_from_span_element(self):
|
||||
span = MagicMock()
|
||||
span.get_attribute.side_effect = lambda name: (
|
||||
SAMPLE if name == "aria-label" else None
|
||||
)
|
||||
span.text = ""
|
||||
span.find_elements.return_value = []
|
||||
assert _url_from_attachment_element(span) == SAMPLE
|
||||
|
||||
def test_sharepoint_download_candidates_include_layouts(self):
|
||||
cands = _sharepoint_download_candidates(SAMPLE)
|
||||
assert any("download.aspx" in c for c in cands)
|
||||
assert any("download=1" in c for c in cands)
|
||||
|
||||
def test_is_pdf_bytes(self):
|
||||
assert _is_pdf_bytes(b"%PDF-1.4 fake content " + b"x" * 6000)
|
||||
assert not _is_pdf_bytes(b"%PDF-1.4 tiny " + b"x" * 100)
|
||||
assert not _is_pdf_bytes(b"<html>viewer</html>")
|
||||
|
||||
def test_resolve_click_target_prefers_href_span(self):
|
||||
span = MagicMock()
|
||||
span.is_displayed.return_value = True
|
||||
span.tag_name = "span"
|
||||
span.get_attribute.side_effect = lambda name: (
|
||||
f"JACKSON ARBRHLS 6.3.26.pdf {SAMPLE}" if name == "aria-label" else None
|
||||
)
|
||||
span.text = ""
|
||||
span.find_elements.return_value = []
|
||||
|
||||
li = MagicMock()
|
||||
li.tag_name = "li"
|
||||
li.is_displayed.return_value = True
|
||||
li.get_attribute.return_value = None
|
||||
li.text = "JACKSON ARBRHLS 6.3.26.pdf"
|
||||
li.find_elements.return_value = [span]
|
||||
|
||||
assert _resolve_attachment_click_target(li) is span
|
||||
assert "6.3.26.pdf" in _attachment_filename_key(span)
|
||||
|
||||
def test_browser_fetch_saves_pdf(self, tmp_path):
|
||||
driver = MagicMock()
|
||||
pdf = b"%PDF-1.4 " + b"x" * 6000
|
||||
driver.execute_async_script.return_value = {
|
||||
"ok": True,
|
||||
"b64": base64.b64encode(pdf).decode("ascii"),
|
||||
"ct": "application/pdf",
|
||||
"size": len(pdf),
|
||||
}
|
||||
driver.current_window_handle = "h1"
|
||||
driver.window_handles = ["h1"]
|
||||
|
||||
def _exec_script(script, *args):
|
||||
if "window.open" in str(script):
|
||||
driver.window_handles = ["h1", "h2"]
|
||||
|
||||
driver.execute_script.side_effect = _exec_script
|
||||
out = _download_href_via_browser_fetch(driver, tmp_path, SAMPLE, set())
|
||||
assert len(out) == 1
|
||||
assert out[0].exists()
|
||||
assert _is_pdf_bytes(out[0].read_bytes())
|
||||
@@ -0,0 +1,55 @@
|
||||
"""Tests for board column detection helpers."""
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from selenium.common.exceptions import InvalidSelectorException
|
||||
|
||||
from onedrive_planner import (
|
||||
_FIRST_COLUMN_CLUSTER_WIDTH_PX,
|
||||
_element_in_board_container,
|
||||
_parent_element,
|
||||
)
|
||||
|
||||
|
||||
def test_first_column_cluster_width_reasonable():
|
||||
assert _FIRST_COLUMN_CLUSTER_WIDTH_PX >= 150
|
||||
|
||||
|
||||
def test_parent_element_stops_at_html_document_error():
|
||||
"""ChromeDriver rejects .. on <html> with InvalidSelectorException."""
|
||||
html = MagicMock()
|
||||
html.tag_name = "html"
|
||||
html.find_element.side_effect = InvalidSelectorException(
|
||||
'The result of the xpath expression ".." is: [object HTMLDocument].'
|
||||
)
|
||||
assert _parent_element(html) is None
|
||||
|
||||
|
||||
def test_parent_element_returns_parent_for_body():
|
||||
body = MagicMock()
|
||||
body.tag_name = "body"
|
||||
parent = MagicMock()
|
||||
parent.tag_name = "html"
|
||||
body.find_element.return_value = parent
|
||||
assert _parent_element(body) is parent
|
||||
|
||||
|
||||
def test_element_in_board_container_does_not_crash_at_root():
|
||||
"""Containment walk must not raise when ancestors hit document root."""
|
||||
child = MagicMock()
|
||||
child.tag_name = "div"
|
||||
container = MagicMock()
|
||||
container.tag_name = "div"
|
||||
|
||||
html = MagicMock()
|
||||
html.tag_name = "html"
|
||||
html.find_element.side_effect = InvalidSelectorException("document root")
|
||||
|
||||
body = MagicMock()
|
||||
body.tag_name = "body"
|
||||
body.find_element.return_value = html
|
||||
|
||||
child.find_element.return_value = body
|
||||
|
||||
assert _element_in_board_container(child, container) is False
|
||||
@@ -0,0 +1,147 @@
|
||||
"""Unit tests for Chromium-only browser resolution."""
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from chrome import (
|
||||
PROJECT_ROOT,
|
||||
_allow_google_chrome_fallback,
|
||||
_browser_runs_in_background,
|
||||
_bundled_chromedriver_for,
|
||||
_google_chrome_fallback_paths,
|
||||
_installed_chromium_paths,
|
||||
_portable_chromium_executable,
|
||||
_prefer_installed_chromium,
|
||||
capture_fullscreen_screenshot,
|
||||
find_chromium_executable,
|
||||
)
|
||||
|
||||
|
||||
def test_installed_paths_exclude_google():
|
||||
installed = _installed_chromium_paths()
|
||||
chrome_fb = _google_chrome_fallback_paths()
|
||||
assert not any("Google" in p for p in installed)
|
||||
assert any("Google" in p for p in chrome_fb)
|
||||
|
||||
|
||||
def test_portable_chrome_win_preferred_over_installed():
|
||||
portable = str(PROJECT_ROOT / "chrome-win" / "chrome-win" / "chrome.exe")
|
||||
installed = r"C:\Program Files\Chromium\Application\chrome.exe"
|
||||
|
||||
def fake_isfile(path: str) -> bool:
|
||||
return path in {portable, installed}
|
||||
|
||||
env = {"USERPROFILE": r"C:\Users\test", "LOCALAPPDATA": r"C:\Users\test\AppData\Local"}
|
||||
with patch("chrome._portable_chromium_executable", return_value=portable):
|
||||
with patch("chrome.os.path.isfile", side_effect=fake_isfile):
|
||||
with patch.dict("os.environ", env, clear=True):
|
||||
assert find_chromium_executable() == portable
|
||||
|
||||
|
||||
def test_installed_used_when_portable_missing():
|
||||
installed = r"C:\Program Files\Chromium\Application\chrome.exe"
|
||||
|
||||
def fake_isfile(path: str) -> bool:
|
||||
return path == installed
|
||||
|
||||
env = {"USERPROFILE": r"C:\Users\test", "LOCALAPPDATA": r"C:\Users\test\AppData\Local"}
|
||||
with patch("chrome._portable_chromium_executable", return_value=None):
|
||||
with patch("chrome.os.path.isfile", side_effect=fake_isfile):
|
||||
with patch.dict("os.environ", env, clear=True):
|
||||
assert find_chromium_executable() == installed
|
||||
|
||||
|
||||
def test_misf_use_installed_chrome_skips_portable():
|
||||
portable = str(PROJECT_ROOT / "chrome-win" / "chrome-win" / "chrome.exe")
|
||||
installed = r"C:\Program Files\Chromium\Application\chrome.exe"
|
||||
|
||||
def fake_isfile(path: str) -> bool:
|
||||
return path in {portable, installed}
|
||||
|
||||
env = {
|
||||
"MISF_USE_INSTALLED_CHROME": "1",
|
||||
"USERPROFILE": r"C:\Users\test",
|
||||
"LOCALAPPDATA": r"C:\Users\test\AppData\Local",
|
||||
}
|
||||
with patch("chrome._portable_chromium_executable", return_value=portable):
|
||||
with patch("chrome.os.path.isfile", side_effect=fake_isfile):
|
||||
with patch.dict("os.environ", env, clear=True):
|
||||
assert find_chromium_executable() == installed
|
||||
|
||||
|
||||
def test_find_chromium_can_use_google_with_allow_flag():
|
||||
google_path = r"C:\Program Files\Google\Chrome\Application\chrome.exe"
|
||||
|
||||
env = {
|
||||
"MISF_ALLOW_CHROME": "1",
|
||||
"USERPROFILE": r"C:\Users\test",
|
||||
"LOCALAPPDATA": r"C:\Users\test\AppData\Local",
|
||||
}
|
||||
with patch("chrome._portable_chromium_executable", return_value=None):
|
||||
with patch("chrome.os.path.isfile", side_effect=lambda p: p == google_path):
|
||||
with patch.dict("os.environ", env, clear=True):
|
||||
assert find_chromium_executable() == google_path
|
||||
|
||||
|
||||
def test_bundled_chromedriver_next_to_portable_chrome():
|
||||
browser = PROJECT_ROOT / "chrome-win" / "chrome-win" / "chrome.exe"
|
||||
if not browser.is_file():
|
||||
return
|
||||
driver = _bundled_chromedriver_for(str(browser))
|
||||
assert driver is not None
|
||||
assert driver.endswith("chromedriver.exe")
|
||||
|
||||
|
||||
def test_portable_chromium_executable_on_disk():
|
||||
exe = _portable_chromium_executable()
|
||||
expected = PROJECT_ROOT / "chrome-win" / "chrome-win" / "chrome.exe"
|
||||
if expected.is_file():
|
||||
assert exe == str(expected)
|
||||
|
||||
|
||||
def test_foreground_default_background_opt_in():
|
||||
with patch.dict("os.environ", {}, clear=True):
|
||||
assert _browser_runs_in_background() is False
|
||||
with patch.dict("os.environ", {"MISF_BROWSER_BACKGROUND": "1"}, clear=True):
|
||||
assert _browser_runs_in_background() is True
|
||||
|
||||
|
||||
def test_allow_chrome_flag():
|
||||
with patch.dict("os.environ", {}, clear=True):
|
||||
assert _allow_google_chrome_fallback() is False
|
||||
with patch.dict("os.environ", {"MISF_ALLOW_CHROME": "yes"}, clear=True):
|
||||
assert _allow_google_chrome_fallback() is True
|
||||
|
||||
|
||||
def test_prefer_installed_flag():
|
||||
with patch.dict("os.environ", {}, clear=True):
|
||||
assert _prefer_installed_chromium() is False
|
||||
with patch.dict("os.environ", {"MISF_USE_INSTALLED_CHROME": "1"}, clear=True):
|
||||
assert _prefer_installed_chromium() is True
|
||||
|
||||
|
||||
def test_fullscreen_screenshot_maximizes_and_uses_desktop_when_not_black(tmp_path):
|
||||
driver = MagicMock()
|
||||
shot = tmp_path / "shot.png"
|
||||
shot.write_bytes(b"\xff" * 100)
|
||||
|
||||
with patch.dict("os.environ", {}, clear=True):
|
||||
with patch("chrome.capture_desktop_screenshot", return_value=True) as cap:
|
||||
with patch("chrome._is_image_mostly_black", return_value=False):
|
||||
assert capture_fullscreen_screenshot(driver, shot) is True
|
||||
cap.assert_called_once_with(shot)
|
||||
driver.maximize_window.assert_called()
|
||||
|
||||
|
||||
def test_fullscreen_screenshot_falls_back_when_desktop_is_black(tmp_path):
|
||||
driver = MagicMock()
|
||||
shot = tmp_path / "shot.png"
|
||||
shot.write_bytes(b"\x00" * 100)
|
||||
|
||||
with patch.dict("os.environ", {}, clear=True):
|
||||
with patch("chrome.capture_desktop_screenshot", return_value=True):
|
||||
with patch("chrome._is_image_mostly_black", return_value=True):
|
||||
with patch("chrome._capture_browser_with_timestamp_bar", return_value=True) as fb:
|
||||
assert capture_fullscreen_screenshot(driver, shot) is True
|
||||
fb.assert_called_once()
|
||||
@@ -0,0 +1,48 @@
|
||||
"""Config and facility list tests (no browser)."""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
SRC = ROOT / "src"
|
||||
sys.path.insert(0, str(SRC))
|
||||
|
||||
from config import ( # noqa: E402
|
||||
read_app_config,
|
||||
read_facility_list,
|
||||
read_onedrive_config,
|
||||
)
|
||||
from onedrive_planner import _sanitize_folder_name, _xpath_literal # noqa: E402
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def example_config() -> Path:
|
||||
path = ROOT / "Input" / "input.xlsx.example"
|
||||
assert path.exists(), f"Missing template: {path}"
|
||||
return path
|
||||
|
||||
|
||||
def test_helpers() -> None:
|
||||
assert _sanitize_folder_name("Test/Facility") == "Test_Facility"
|
||||
assert _xpath_literal("Plain Name") == "'Plain Name'"
|
||||
assert "concat" in _xpath_literal('O\'Brien "East" Wing')
|
||||
|
||||
|
||||
def test_read_facility_list_skips_placeholder(example_config: Path) -> None:
|
||||
facilities = read_facility_list(example_config, warn_if_not_expected_count=False)
|
||||
assert facilities
|
||||
assert not any("REPLACE_WITH" in f for f in facilities)
|
||||
|
||||
|
||||
def test_read_app_config_planner_url(example_config: Path) -> None:
|
||||
cfg = read_app_config(example_config)
|
||||
assert "planner.cloud.microsoft" in cfg.planner_url
|
||||
|
||||
|
||||
def test_read_onedrive_config_tuple_length(example_config: Path) -> None:
|
||||
row = read_onedrive_config(example_config)
|
||||
assert len(row) == 10
|
||||
assert "planner.cloud.microsoft" in row[9]
|
||||
@@ -0,0 +1,36 @@
|
||||
"""Smoke tests for OneDrive download config (no browser)."""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
SRC = ROOT / "src"
|
||||
sys.path.insert(0, str(SRC))
|
||||
|
||||
from config import CONFIG_FILE, read_app_config, read_facility_list # noqa: E402
|
||||
from onedrive_planner import _sanitize_folder_name, _xpath_literal # noqa: E402
|
||||
|
||||
|
||||
def test_helpers() -> None:
|
||||
assert _sanitize_folder_name("Test/Facility") == "Test_Facility"
|
||||
assert _xpath_literal("Plain Name") == "'Plain Name'"
|
||||
assert _xpath_literal("O'Brien Care") == '"O\'Brien Care"'
|
||||
assert "concat" in _xpath_literal('O\'Brien "East" Wing')
|
||||
print("OK: helper functions")
|
||||
|
||||
|
||||
def test_config_loads() -> None:
|
||||
path = CONFIG_FILE if CONFIG_FILE.exists() else ROOT / "Input" / "input.xlsx.example"
|
||||
assert path.exists(), f"Missing config: {path}"
|
||||
facilities = read_facility_list(path)
|
||||
assert len(facilities) >= 1
|
||||
cfg = read_app_config(path)
|
||||
assert cfg.planner_url
|
||||
print(f"OK: {len(facilities)} facilities from {path.name}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_helpers()
|
||||
test_config_loads()
|
||||
print("All smoke tests passed.")
|
||||
@@ -0,0 +1,105 @@
|
||||
"""Tests for download summary Excel columns."""
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from planner_download import (
|
||||
_build_workable_files,
|
||||
_format_status_changed,
|
||||
_success_status,
|
||||
_write_download_summary,
|
||||
)
|
||||
|
||||
|
||||
def test_success_status_labels():
|
||||
assert _success_status(0, 0) == "No tasks"
|
||||
assert _success_status(0, 3, tasks_skipped_no_attachment=3) == "No attachments"
|
||||
assert _success_status(1, 3, tasks_with_downloads=1, tasks_with_attachments=1) == "Success"
|
||||
assert _success_status(2, 4, tasks_with_downloads=2, tasks_with_attachments=2) == "Success"
|
||||
assert _success_status(1, 4, tasks_with_downloads=1, tasks_with_attachments=2) == "Partial"
|
||||
assert _success_status(0, 2, tasks_with_attachments=2) == "Download failed"
|
||||
|
||||
|
||||
def test_format_status_changed():
|
||||
assert _format_status_changed(0, 0) == "N/A"
|
||||
assert _format_status_changed(2, 2, via_task_details=2) == "Yes - 2/2 tasks (2 via task details)"
|
||||
assert _format_status_changed(3, 1, via_task_details=1) == "Partial - 1/3 tasks (1 via task details)"
|
||||
assert _format_status_changed(1, 0) == "No - 0/1 tasks"
|
||||
assert _format_status_changed(2, 1, via_task_details=1) == "Partial - 1/2 tasks (1 via task details)"
|
||||
|
||||
|
||||
def test_write_download_summary_columns(tmp_path: Path, monkeypatch):
|
||||
monkeypatch.setattr("planner_download.PROJECT_ROOT", tmp_path)
|
||||
results = {
|
||||
"Alpha": {
|
||||
"original": [tmp_path / "a.pdf", tmp_path / "b.pdf"],
|
||||
"tasks_total": 4,
|
||||
"tasks_with_attachments": 2,
|
||||
"tasks_skipped_no_attachment": 2,
|
||||
"tasks_with_downloads": 2,
|
||||
"status_changed": 1,
|
||||
"status_via_task_details": 1,
|
||||
},
|
||||
"Beta": {
|
||||
"original": [],
|
||||
"tasks_total": 2,
|
||||
"tasks_with_attachments": 0,
|
||||
"tasks_skipped_no_attachment": 2,
|
||||
"tasks_with_downloads": 0,
|
||||
"status_changed": 0,
|
||||
"status_via_task_details": 0,
|
||||
},
|
||||
}
|
||||
failures = [("Gamma", "not found", "Plan not found")]
|
||||
summary_path = tmp_path / "Summary" / "Download_Summary_2026-06-05.xlsx"
|
||||
_write_download_summary(
|
||||
results,
|
||||
failures,
|
||||
summary_path,
|
||||
planner_url="https://example.com",
|
||||
run_timestamp="2026-06-05 12:00:00",
|
||||
)
|
||||
df = pd.read_excel(summary_path, keep_default_na=False)
|
||||
assert list(df.columns) == [
|
||||
"Run Timestamp",
|
||||
"Facility Name",
|
||||
"Files Downloaded",
|
||||
"Status",
|
||||
"Status Changed",
|
||||
]
|
||||
alpha = df[df["Facility Name"] == "Alpha"].iloc[0]
|
||||
assert alpha["Status"] == "Success"
|
||||
assert "1/2 tasks" in alpha["Status Changed"]
|
||||
assert "via task details" in alpha["Status Changed"]
|
||||
beta = df[df["Facility Name"] == "Beta"].iloc[0]
|
||||
assert beta["Status"] == "No attachments"
|
||||
assert beta["Status Changed"] == "N/A"
|
||||
gamma = df[df["Facility Name"] == "Gamma"].iloc[0]
|
||||
assert gamma["Status"] == "Plan not found"
|
||||
assert gamma["Status Changed"] == "N/A"
|
||||
|
||||
|
||||
def test_build_workable_files_mirrors_to_medicare(tmp_path: Path):
|
||||
src_dir = tmp_path / "original" / "FacA"
|
||||
src_dir.mkdir(parents=True)
|
||||
pdf = src_dir / "doc.pdf"
|
||||
pdf.write_bytes(b"%PDF-1.4")
|
||||
|
||||
paths, workable_root = _build_workable_files(
|
||||
facility_name="Fac A",
|
||||
downloaded=[pdf],
|
||||
output_root=tmp_path / "Output",
|
||||
today_iso="2026-06-16",
|
||||
)
|
||||
medicare_root = tmp_path / "Output" / "2026-06-16" / "Workable_Medicare"
|
||||
|
||||
assert len(paths) == 1
|
||||
assert paths[0].parent == workable_root
|
||||
assert paths[0].is_file()
|
||||
assert medicare_root.is_dir()
|
||||
medicare_files = list(medicare_root.glob("*.pdf"))
|
||||
assert len(medicare_files) == 1
|
||||
assert medicare_files[0].name == paths[0].name
|
||||
assert medicare_files[0].read_bytes() == pdf.read_bytes()
|
||||
@@ -0,0 +1,55 @@
|
||||
"""Unit tests for Shared plans facility search helpers."""
|
||||
from __future__ import annotations
|
||||
|
||||
from onedrive_planner import (
|
||||
FACILITY_NOT_APPEAR_MSG,
|
||||
_facility_search_terms,
|
||||
_row_matches_facility_search,
|
||||
)
|
||||
|
||||
|
||||
def test_facility_search_terms_full_then_short():
|
||||
terms = _facility_search_terms("Golden Hill Post Acute Care")
|
||||
assert terms == [
|
||||
"Golden Hill Post Acute Care",
|
||||
"Golden Hill Post",
|
||||
"Golden Hill",
|
||||
]
|
||||
|
||||
|
||||
def test_facility_search_terms_two_words_only():
|
||||
terms = _facility_search_terms("Arbor Hills")
|
||||
assert terms == ["Arbor Hills"]
|
||||
|
||||
|
||||
def test_facility_search_terms_dedupes():
|
||||
terms = _facility_search_terms("Golden Hill")
|
||||
assert terms == ["Golden Hill"]
|
||||
|
||||
|
||||
def test_row_matches_full_name_in_plan_title():
|
||||
assert _row_matches_facility_search(
|
||||
"Golden Hill Subacute and Rehab",
|
||||
"Golden Hill Post Acute Care",
|
||||
"Golden Hill",
|
||||
)
|
||||
|
||||
|
||||
def test_row_matches_exact():
|
||||
assert _row_matches_facility_search(
|
||||
"Arbor Hills",
|
||||
"Arbor Hills",
|
||||
"Arbor Hills",
|
||||
)
|
||||
|
||||
|
||||
def test_row_matches_rejects_unrelated():
|
||||
assert not _row_matches_facility_search(
|
||||
"Sunrise Manor",
|
||||
"Golden Hill Post Acute Care",
|
||||
"Golden Hill",
|
||||
)
|
||||
|
||||
|
||||
def test_not_appear_message_constant():
|
||||
assert "shared plans" in FACILITY_NOT_APPEAR_MSG.lower()
|
||||
@@ -0,0 +1,202 @@
|
||||
"""Unit tests for Planner Grid view helpers."""
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from selenium.webdriver.common.by import By
|
||||
|
||||
from onedrive_planner import (
|
||||
_clean_grid_task_title,
|
||||
_grid_row_status,
|
||||
_grid_row_task_name,
|
||||
_is_attachments_empty,
|
||||
_is_panel_placeholder_text,
|
||||
_is_status_sorted_descending,
|
||||
_parse_status_blob,
|
||||
_sort_descending_menu_selected,
|
||||
)
|
||||
|
||||
|
||||
def _grid_row(*, status_value: str = "", status_text: str = "", task_name: str = ""):
|
||||
row = MagicMock()
|
||||
row.is_displayed.return_value = True
|
||||
row.text = task_name
|
||||
|
||||
status_btn = MagicMock()
|
||||
status_btn.is_displayed.return_value = True
|
||||
status_btn.get_attribute.side_effect = lambda name: (
|
||||
status_value if name == "value" else ""
|
||||
)
|
||||
status_btn.text = status_text
|
||||
|
||||
name_cell = MagicMock()
|
||||
name_cell.is_displayed.return_value = True
|
||||
name_cell.get_attribute.return_value = f"Task Name {task_name}"
|
||||
name_span = MagicMock()
|
||||
name_span.is_displayed.return_value = True
|
||||
name_span.text = task_name
|
||||
|
||||
def find_elements(by, xpath):
|
||||
xp = xpath or ""
|
||||
if "combobox" in xp:
|
||||
return [status_btn]
|
||||
if "gridcell" in xp and "span" not in xp:
|
||||
return [name_cell]
|
||||
if "span" in xp:
|
||||
return [name_span]
|
||||
return []
|
||||
|
||||
row.find_elements.side_effect = find_elements
|
||||
name_cell.find_elements.side_effect = lambda by, xpath: [name_span]
|
||||
return row
|
||||
|
||||
|
||||
def test_grid_row_status_not_started():
|
||||
row = _grid_row(status_value="notStarted", status_text="Not started")
|
||||
assert _grid_row_status(row) == "not started"
|
||||
|
||||
|
||||
def test_grid_row_status_reads_status_column_not_bucket():
|
||||
row = MagicMock()
|
||||
row.is_displayed.return_value = True
|
||||
|
||||
bucket_btn = MagicMock()
|
||||
bucket_btn.is_displayed.return_value = True
|
||||
bucket_btn.get_attribute.side_effect = lambda name: "todo" if name == "value" else ""
|
||||
bucket_btn.text = "To do"
|
||||
|
||||
status_btn = MagicMock()
|
||||
status_btn.is_displayed.return_value = True
|
||||
status_btn.get_attribute.side_effect = lambda name: "notStarted" if name == "value" else ""
|
||||
status_btn.text = "Not started"
|
||||
|
||||
status_cell = MagicMock()
|
||||
status_cell.is_displayed.return_value = True
|
||||
status_cell.get_attribute.return_value = "Status Not started"
|
||||
status_cell.text = "Not started"
|
||||
status_cell.find_elements.side_effect = lambda by, xpath: (
|
||||
[status_btn] if "combobox" in xpath else []
|
||||
)
|
||||
|
||||
bucket_cell = MagicMock()
|
||||
bucket_cell.is_displayed.return_value = True
|
||||
bucket_cell.get_attribute.return_value = "Bucket To do"
|
||||
bucket_cell.text = "To do"
|
||||
bucket_cell.find_elements.side_effect = lambda by, xpath: (
|
||||
[bucket_btn] if "combobox" in xpath else []
|
||||
)
|
||||
|
||||
def find_elements(by, xpath):
|
||||
xp = xpath or ""
|
||||
if "gridcell" in xp:
|
||||
return [status_cell, bucket_cell]
|
||||
if "combobox" in xp:
|
||||
return [bucket_btn, status_btn]
|
||||
return []
|
||||
|
||||
row.find_elements.side_effect = find_elements
|
||||
assert _grid_row_status(row) == "not started"
|
||||
|
||||
|
||||
def test_grid_row_status_in_progress():
|
||||
row = _grid_row(status_value="inProgress", status_text="In progress")
|
||||
assert _grid_row_status(row) == "in progress"
|
||||
|
||||
|
||||
def test_grid_row_task_name_from_aria():
|
||||
row = _grid_row(task_name="Missing Insurance Info")
|
||||
assert _grid_row_task_name(row) == "Missing Insurance Info"
|
||||
|
||||
|
||||
def test_clean_grid_task_title_strips_a11y_hint():
|
||||
raw = "Missing Insurance Info. Press Alt + I to open task details, or pre"
|
||||
assert _clean_grid_task_title(raw) == "Missing Insurance Info"
|
||||
|
||||
|
||||
def test_grid_row_task_name_strips_a11y_from_span():
|
||||
row = MagicMock()
|
||||
row.is_displayed.return_value = True
|
||||
row.text = ""
|
||||
|
||||
name_cell = MagicMock()
|
||||
name_cell.is_displayed.return_value = True
|
||||
name_cell.get_attribute.return_value = ""
|
||||
name_span = MagicMock()
|
||||
name_span.is_displayed.return_value = True
|
||||
name_span.text = "6.10 priority :). Press Alt + I to open task details"
|
||||
|
||||
def find_elements(by, xpath):
|
||||
xp = xpath or ""
|
||||
if "combobox" in xp:
|
||||
return []
|
||||
if "gridcell" in xp and "span" not in xp:
|
||||
return [name_cell]
|
||||
if "span" in xp:
|
||||
return [name_span]
|
||||
return []
|
||||
|
||||
row.find_elements.side_effect = find_elements
|
||||
name_cell.find_elements.side_effect = lambda by, xpath: [name_span]
|
||||
assert _grid_row_task_name(row) == "6.10 priority :)"
|
||||
|
||||
|
||||
def test_is_panel_placeholder_text():
|
||||
assert _is_panel_placeholder_text("Type a description or add notes here")
|
||||
assert not _is_panel_placeholder_text("Missing Insurance Info")
|
||||
|
||||
|
||||
def test_is_status_sorted_descending():
|
||||
header = MagicMock()
|
||||
header.get_attribute.side_effect = lambda name: (
|
||||
"descending" if name == "aria-sort" else ""
|
||||
)
|
||||
assert _is_status_sorted_descending(header) is True
|
||||
|
||||
header.get_attribute.side_effect = lambda name: (
|
||||
"ascending" if name == "aria-sort" else ""
|
||||
)
|
||||
assert _is_status_sorted_descending(header) is False
|
||||
|
||||
header.get_attribute.side_effect = lambda name: "" if name == "aria-sort" else ""
|
||||
assert _is_status_sorted_descending(header) is False
|
||||
|
||||
|
||||
def test_sort_descending_menu_selected():
|
||||
driver = MagicMock()
|
||||
checked_btn = MagicMock()
|
||||
checked_btn.is_displayed.return_value = True
|
||||
|
||||
driver.find_elements.side_effect = lambda by, xpath: (
|
||||
[checked_btn]
|
||||
if "aria-checked='true'" in xpath
|
||||
else []
|
||||
)
|
||||
assert _sort_descending_menu_selected(driver) is True
|
||||
|
||||
driver.find_elements.side_effect = lambda by, xpath: []
|
||||
assert _sort_descending_menu_selected(driver) is False
|
||||
|
||||
|
||||
def test_parse_status_blob():
|
||||
assert _parse_status_blob("Not started") == "not started"
|
||||
assert _parse_status_blob("Status Completed") == "completed"
|
||||
assert _parse_status_blob("To do") is None
|
||||
|
||||
|
||||
def test_is_attachments_empty_detects_empty_panel():
|
||||
driver = MagicMock()
|
||||
panel = MagicMock()
|
||||
panel.is_displayed.return_value = True
|
||||
panel.text = "Select, upload, or add a link to a file or website\nAdd attachment"
|
||||
driver.find_elements.return_value = [panel]
|
||||
|
||||
with patch("onedrive_planner._attachment_tab_count", return_value=0), patch(
|
||||
"onedrive_planner._discover_listed_attachment_names", return_value=[]
|
||||
), patch("onedrive_planner._list_attachment_rows", return_value=[]):
|
||||
assert _is_attachments_empty(driver) is True
|
||||
|
||||
|
||||
def test_is_attachments_empty_false_when_tab_count():
|
||||
driver = MagicMock()
|
||||
with patch("onedrive_planner._attachment_tab_count", return_value=2):
|
||||
assert _is_attachments_empty(driver) is False
|
||||
@@ -0,0 +1,64 @@
|
||||
"""Unit tests for Microsoft login page detection."""
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from selenium.webdriver.common.by import By
|
||||
|
||||
from onedrive_planner import (
|
||||
_email_form_visible,
|
||||
_needs_microsoft_login,
|
||||
_password_form_visible,
|
||||
_planner_session_ready,
|
||||
)
|
||||
|
||||
|
||||
def _driver(*, url: str = "", elements: dict | None = None):
|
||||
drv = MagicMock()
|
||||
drv.current_url = url
|
||||
elements = elements or {}
|
||||
|
||||
def find_elements(by, sel):
|
||||
key = (by, sel)
|
||||
return elements.get(key, [])
|
||||
|
||||
drv.find_elements.side_effect = find_elements
|
||||
return drv
|
||||
|
||||
|
||||
def _input(displayed: bool = True):
|
||||
el = MagicMock()
|
||||
el.is_displayed.return_value = displayed
|
||||
return el
|
||||
|
||||
|
||||
class TestLoginHelpers:
|
||||
def test_password_only_login_detected(self):
|
||||
drv = _driver(
|
||||
url="https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
|
||||
elements={
|
||||
(By.CSS_SELECTOR, "input[type='password']"): [_input()],
|
||||
},
|
||||
)
|
||||
assert _password_form_visible(drv)
|
||||
assert not _email_form_visible(drv)
|
||||
assert _needs_microsoft_login(drv)
|
||||
assert not _planner_session_ready(drv)
|
||||
|
||||
def test_planner_ready_when_no_login_prompt(self):
|
||||
drv = _driver(
|
||||
url="https://planner.cloud.microsoft/webui/mytasks/assignedtome/view/grid",
|
||||
elements={},
|
||||
)
|
||||
assert not _needs_microsoft_login(drv)
|
||||
assert _planner_session_ready(drv)
|
||||
|
||||
def test_planner_url_with_password_still_needs_login(self):
|
||||
drv = _driver(
|
||||
url="https://planner.cloud.microsoft/webui/mytasks/assignedtome/view/grid",
|
||||
elements={
|
||||
(By.CSS_SELECTOR, "input[type='password']"): [_input()],
|
||||
},
|
||||
)
|
||||
assert _needs_microsoft_login(drv)
|
||||
assert not _planner_session_ready(drv)
|
||||
@@ -0,0 +1,68 @@
|
||||
"""Unit tests for Planner left-nav helpers."""
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from selenium.webdriver.common.by import By
|
||||
|
||||
from onedrive_planner import (
|
||||
_left_nav_pane_collapsed,
|
||||
_my_plans_nav_visible,
|
||||
)
|
||||
|
||||
|
||||
def _nav_toggle(*, expanded: str = "false", displayed: bool = True):
|
||||
btn = MagicMock()
|
||||
btn.is_displayed.return_value = displayed
|
||||
btn.get_attribute.side_effect = lambda name: expanded if name == "aria-expanded" else ""
|
||||
return btn
|
||||
|
||||
|
||||
def _nav_link(*, displayed: bool = True, enabled: bool = True):
|
||||
el = MagicMock()
|
||||
el.is_displayed.return_value = displayed
|
||||
el.is_enabled.return_value = enabled
|
||||
return el
|
||||
|
||||
|
||||
def _mock_driver(*, elements: dict):
|
||||
drv = MagicMock()
|
||||
drv.find_elements.side_effect = lambda by, sel: elements.get((by, sel), [])
|
||||
return drv
|
||||
|
||||
|
||||
class TestNavHelpers:
|
||||
def test_collapsed_when_aria_expanded_false(self):
|
||||
drv = _mock_driver(
|
||||
elements={
|
||||
(By.XPATH, "//button[@aria-label='Left navigation pane']"): [
|
||||
_nav_toggle(expanded="false")
|
||||
],
|
||||
}
|
||||
)
|
||||
assert _left_nav_pane_collapsed(drv)
|
||||
|
||||
def test_not_collapsed_when_aria_expanded_true(self):
|
||||
drv = _mock_driver(
|
||||
elements={
|
||||
(By.XPATH, "//button[@aria-label='Left navigation pane']"): [
|
||||
_nav_toggle(expanded="true")
|
||||
],
|
||||
}
|
||||
)
|
||||
assert not _left_nav_pane_collapsed(drv)
|
||||
|
||||
def test_my_plans_visible(self):
|
||||
drv = _mock_driver(
|
||||
elements={
|
||||
(
|
||||
By.XPATH,
|
||||
"//*[self::span or self::div][normalize-space()='My plans']",
|
||||
): [_nav_link()],
|
||||
}
|
||||
)
|
||||
assert _my_plans_nav_visible(drv)
|
||||
|
||||
def test_my_plans_hidden(self):
|
||||
drv = _mock_driver(elements={})
|
||||
assert not _my_plans_nav_visible(drv)
|
||||
@@ -0,0 +1,23 @@
|
||||
"""Tests for Planner tab URL classification."""
|
||||
from __future__ import annotations
|
||||
|
||||
from onedrive_planner import _is_facility_plan_url, _is_planner_hub_url
|
||||
|
||||
|
||||
def test_facility_plan_url():
|
||||
assert _is_facility_plan_url(
|
||||
"https://planner.cloud.microsoft/webui/plan/PID_ABC/view/board?tid=x"
|
||||
)
|
||||
assert not _is_facility_plan_url(
|
||||
"https://planner.cloud.microsoft/webui/mytasks/assignedtome/view/grid?tid=x"
|
||||
)
|
||||
|
||||
|
||||
def test_planner_hub_url():
|
||||
assert _is_planner_hub_url(
|
||||
"https://planner.cloud.microsoft/webui/mytasks/assignedtome/view/grid"
|
||||
)
|
||||
assert not _is_planner_hub_url(
|
||||
"https://planner.cloud.microsoft/webui/plan/PID_ABC/view/board"
|
||||
)
|
||||
assert not _is_planner_hub_url("about:blank")
|
||||
@@ -0,0 +1,13 @@
|
||||
"""Tests for step logging helper."""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from run_log import log_step
|
||||
|
||||
|
||||
def test_log_step_formats_message(caplog):
|
||||
caplog.set_level(logging.INFO, logger="misf.run")
|
||||
log_step("Open facility", facility="Amaya", rows=2)
|
||||
assert any("[STEP] Open facility" in r.message for r in caplog.records)
|
||||
assert any("facility=Amaya" in r.message for r in caplog.records)
|
||||
@@ -0,0 +1,306 @@
|
||||
"""Unit tests for board task filtering (not started + attachment)."""
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
from selenium.common.exceptions import NoSuchElementException
|
||||
from selenium.webdriver.common.by import By
|
||||
|
||||
from onedrive_planner import (
|
||||
_card_has_attachment,
|
||||
_is_active_bucket_title,
|
||||
_is_completed_tasks_header,
|
||||
_is_in_active_planner_column_group,
|
||||
_is_in_completed_tasks_section,
|
||||
_is_probable_single_task_card,
|
||||
_qualifies_not_started_with_attachment,
|
||||
_task_card_matches_title,
|
||||
_task_has_unchecked_complete_checkbox,
|
||||
_task_is_in_progress,
|
||||
_task_is_not_started,
|
||||
)
|
||||
|
||||
|
||||
def _card(
|
||||
*,
|
||||
text: str = "",
|
||||
aria: str = "",
|
||||
attachment_child: bool = False,
|
||||
checkbox_checked: bool = False,
|
||||
progress_status: str | None = None,
|
||||
):
|
||||
el = MagicMock()
|
||||
el.tag_name = "div"
|
||||
el.text = text
|
||||
el.is_displayed.return_value = True
|
||||
el.get_attribute.return_value = aria
|
||||
textbox = MagicMock()
|
||||
textbox.is_displayed.return_value = True
|
||||
textbox.text = text
|
||||
textbox.get_attribute.return_value = ""
|
||||
attach = MagicMock()
|
||||
attach.is_displayed.return_value = True
|
||||
attach.text = ""
|
||||
attach.get_attribute.return_value = "1 attachment"
|
||||
checkbox = MagicMock()
|
||||
checkbox.is_displayed.return_value = True
|
||||
checkbox.text = ""
|
||||
checkbox.get_attribute.side_effect = lambda name: (
|
||||
"true" if checkbox_checked else "false"
|
||||
if name == "aria-checked"
|
||||
else "Complete task"
|
||||
if name == "aria-label"
|
||||
else ""
|
||||
)
|
||||
|
||||
progress_btn = None
|
||||
if progress_status == "in_progress":
|
||||
progress_btn = MagicMock()
|
||||
progress_btn.is_displayed.return_value = True
|
||||
progress_btn.get_attribute.side_effect = lambda name: (
|
||||
"In progress" if name == "aria-label" else ""
|
||||
)
|
||||
elif progress_status == "not_started":
|
||||
progress_btn = MagicMock()
|
||||
progress_btn.is_displayed.return_value = True
|
||||
progress_btn.get_attribute.side_effect = lambda name: (
|
||||
"Not started" if name == "aria-label" else ""
|
||||
)
|
||||
|
||||
def find_element(by, xpath):
|
||||
if xpath == "./..":
|
||||
raise NoSuchElementException
|
||||
raise NoSuchElementException
|
||||
|
||||
el.find_element.side_effect = find_element
|
||||
|
||||
def _find_elements(by, xpath):
|
||||
xp = xpath or ""
|
||||
if xpath == "./..":
|
||||
raise NoSuchElementException
|
||||
if "textbox" in xp:
|
||||
return [textbox] if text else []
|
||||
if "attachment-indicator" in xp:
|
||||
return [attach] if attachment_child else []
|
||||
if "role='checkbox'" in xp or '@role="checkbox"' in xp:
|
||||
if "@aria-checked='true'" in xp or 'aria-checked="true"' in xp:
|
||||
return [checkbox] if checkbox_checked else []
|
||||
if "@aria-checked='false'" in xp or 'aria-checked="false"' in xp:
|
||||
return [] if checkbox_checked else [checkbox]
|
||||
if "aria-checked='false'" in xp and "|" in xp:
|
||||
return [] if checkbox_checked else [checkbox]
|
||||
return [checkbox]
|
||||
if progress_btn is not None and "button" in xp and "aria-label" in xp:
|
||||
if progress_status == "in_progress" and "In progress" in xp:
|
||||
return [progress_btn]
|
||||
if progress_status == "not_started" and "Not started" in xp:
|
||||
return [progress_btn]
|
||||
return []
|
||||
if "Attachment" in xp or "Attach" in xp or "attachment" in xp:
|
||||
return [attach] if attachment_child else []
|
||||
if "completed tasks" in xp.lower():
|
||||
return []
|
||||
return []
|
||||
|
||||
el.find_elements.side_effect = _find_elements
|
||||
return el
|
||||
|
||||
|
||||
class TestTaskFilter:
|
||||
def test_not_started_with_paperclip_no_label_text(self):
|
||||
card = _card(text="5.29.26", aria="Task card", attachment_child=True)
|
||||
assert _card_has_attachment(card)
|
||||
assert _task_is_not_started(card)
|
||||
assert not _task_is_in_progress(card)
|
||||
|
||||
def test_skips_in_progress(self):
|
||||
card = _card(text="Patient", aria="In progress, 1 attachment", attachment_child=True)
|
||||
assert _task_is_in_progress(card)
|
||||
assert not _task_is_not_started(card)
|
||||
|
||||
def test_skips_half_blue_in_progress_button(self):
|
||||
"""Planner half-blue ring: button aria-label='In progress' (not the complete checkbox)."""
|
||||
card = _card(
|
||||
text="Abaite, Stephen 1/30/54-Need Insurance",
|
||||
attachment_child=True,
|
||||
progress_status="in_progress",
|
||||
)
|
||||
from onedrive_planner import _task_has_in_progress_status_button
|
||||
|
||||
assert _task_has_in_progress_status_button(card)
|
||||
assert _task_is_in_progress(card)
|
||||
assert not _task_is_not_started(card)
|
||||
assert not _qualifies_not_started_with_attachment(card, from_indicator=True)
|
||||
|
||||
def test_processes_not_started_without_progress_button(self):
|
||||
card = _card(
|
||||
text="Richardson, Steve 1/21/56-Need Insurance",
|
||||
attachment_child=True,
|
||||
)
|
||||
from onedrive_planner import _task_has_in_progress_status_button
|
||||
|
||||
assert not _task_has_in_progress_status_button(card)
|
||||
assert _task_is_not_started(card)
|
||||
assert _qualifies_not_started_with_attachment(card)
|
||||
|
||||
def test_skips_mixed_checkbox_in_progress(self):
|
||||
card = _card(text="6.1.26", attachment_child=True)
|
||||
checkbox = card.find_elements(By.XPATH, ".//label[@role='checkbox']")[0]
|
||||
checkbox.get_attribute.side_effect = lambda name: (
|
||||
"mixed"
|
||||
if name == "aria-checked"
|
||||
else "Complete task"
|
||||
if name == "aria-label"
|
||||
else ""
|
||||
)
|
||||
from onedrive_planner import _task_is_in_progress_scoped
|
||||
|
||||
assert _task_is_in_progress_scoped(card)
|
||||
assert not _task_is_not_started(card)
|
||||
|
||||
def test_skips_completed(self):
|
||||
card = _card(text="Done", aria="Completed", attachment_child=True, checkbox_checked=True)
|
||||
assert not _task_is_not_started(card)
|
||||
|
||||
def test_checkbox_unchecked_is_not_started(self):
|
||||
card = _card(text="5.29.26", attachment_child=True, checkbox_checked=False)
|
||||
assert _task_has_unchecked_complete_checkbox(card)
|
||||
assert _task_is_not_started(card)
|
||||
|
||||
def test_not_started_progress_ring_checkbox_qualifies(self):
|
||||
"""Planner progress ring uses role=checkbox aria-label='Not started'."""
|
||||
card = _card(text="6.3.26", attachment_child=True, progress_status="not_started")
|
||||
checkbox = card.find_elements(By.XPATH, ".//label[@role='checkbox']")[0]
|
||||
checkbox.get_attribute.side_effect = lambda name: (
|
||||
"false"
|
||||
if name == "aria-checked"
|
||||
else "Not started"
|
||||
if name == "aria-label"
|
||||
else ""
|
||||
)
|
||||
from onedrive_planner import _filter_reject_reason, _task_is_strictly_not_started
|
||||
|
||||
assert _task_has_unchecked_complete_checkbox(card)
|
||||
assert _task_is_strictly_not_started(card)
|
||||
assert _filter_reject_reason(card, from_indicator=True) is None
|
||||
assert _qualifies_not_started_with_attachment(card, from_indicator=True)
|
||||
|
||||
def test_planner_button_complete_checkbox_is_not_started(self):
|
||||
"""Planner 2024+ uses button[role=checkbox] aria-label='Complete task'."""
|
||||
card = _card(text="6.8.26", attachment_child=True, checkbox_checked=False)
|
||||
checkbox = card.find_elements(
|
||||
By.XPATH,
|
||||
".//label[@role='checkbox'] | .//button[@role='checkbox']",
|
||||
)[0]
|
||||
checkbox.get_attribute.side_effect = lambda name: (
|
||||
"false"
|
||||
if name == "aria-checked"
|
||||
else "Complete task"
|
||||
if name == "aria-label"
|
||||
else ""
|
||||
)
|
||||
from onedrive_planner import _filter_reject_reason, _task_has_unchecked_complete_checkbox
|
||||
|
||||
assert _task_has_unchecked_complete_checkbox(card)
|
||||
assert _filter_reject_reason(card, from_indicator=True) is None
|
||||
|
||||
def test_in_progress_rejected_on_board_without_opening(self):
|
||||
"""Half-blue In progress ring on the card — skip at board scan."""
|
||||
card = _card(
|
||||
text="Endersby, Mary",
|
||||
attachment_child=True,
|
||||
progress_status="in_progress",
|
||||
)
|
||||
from onedrive_planner import (
|
||||
_filter_reject_reason,
|
||||
_task_card_in_progress_on_board,
|
||||
_task_is_strictly_not_started,
|
||||
)
|
||||
|
||||
assert _task_card_in_progress_on_board(card)
|
||||
assert not _task_is_strictly_not_started(card)
|
||||
assert _filter_reject_reason(card, from_indicator=True) == "in_progress"
|
||||
assert _filter_reject_reason(card, from_indicator=False) == "in_progress"
|
||||
assert not _qualifies_not_started_with_attachment(card, from_indicator=True)
|
||||
|
||||
def test_requires_attachment(self):
|
||||
card = _card(text="5.29.26", aria="Not started")
|
||||
assert not _card_has_attachment(card)
|
||||
|
||||
def test_completed_header_matcher(self):
|
||||
assert _is_completed_tasks_header("Completed tasks")
|
||||
assert _is_completed_tasks_header("Completed tasks 37")
|
||||
assert not _is_completed_tasks_header("To do 5.29.26 Completed tasks section")
|
||||
|
||||
def test_indicator_qualifies_without_board_completed_noise(self):
|
||||
ind = _card(text="6.3.26", aria="1 attachment", attachment_child=True)
|
||||
assert _is_probable_single_task_card(ind)
|
||||
assert _qualifies_not_started_with_attachment(ind, from_indicator=True)
|
||||
|
||||
def test_unbucketed_active_group_not_completed_section(self):
|
||||
"""Tasks in taskBoardColumnGroup-unbucketed must not match completed subsection."""
|
||||
from selenium.webdriver.common.by import By
|
||||
|
||||
indicator = MagicMock()
|
||||
active_group = MagicMock()
|
||||
active_group.get_attribute.side_effect = lambda name: (
|
||||
"taskBoardColumnGroup-unbucketed" if name == "data-testid" else ""
|
||||
)
|
||||
indicator.get_attribute.return_value = ""
|
||||
|
||||
def _find_elements(by, xp):
|
||||
if by == By.XPATH and "taskBoardColumnGroup-" in xp:
|
||||
if "contains(@data-testid,'-completed')" in xp:
|
||||
return []
|
||||
return [active_group]
|
||||
return []
|
||||
|
||||
indicator.find_elements.side_effect = _find_elements
|
||||
|
||||
assert _is_in_active_planner_column_group(indicator)
|
||||
assert not _is_in_completed_tasks_section(indicator)
|
||||
|
||||
def test_active_bucket_titles(self):
|
||||
assert _is_active_bucket_title("To do")
|
||||
assert _is_active_bucket_title("TO DO")
|
||||
assert _is_active_bucket_title("New Facesheets")
|
||||
assert _is_active_bucket_title("New Facesheets 12")
|
||||
assert _is_active_bucket_title("New Facesheet")
|
||||
assert _is_active_bucket_title("Heather gets facesheets")
|
||||
assert _is_active_bucket_title("No bucket")
|
||||
assert not _is_active_bucket_title("Completed tasks")
|
||||
assert not _is_active_bucket_title("Completed tasks 875")
|
||||
|
||||
def test_bucket_column_detected(self):
|
||||
col = _card(
|
||||
text="To do",
|
||||
aria="To do, Use tab key to access a task card, left and right arrow",
|
||||
attachment_child=True,
|
||||
)
|
||||
from onedrive_planner import _is_planner_bucket_column
|
||||
|
||||
assert _is_planner_bucket_column(col)
|
||||
|
||||
def test_weak_title_key_ignores_paperclip_count(self):
|
||||
from onedrive_planner import _is_weak_task_title_key
|
||||
|
||||
assert _is_weak_task_title_key("3")
|
||||
assert _is_weak_task_title_key("")
|
||||
assert not _is_weak_task_title_key("6.3.26 Missing Insurance Info")
|
||||
|
||||
def test_board_wrapper_not_single_task_card(self):
|
||||
board = _card(text="To do\n6.3.26\nCompleted tasks 37", attachment_child=True)
|
||||
board.find_elements.side_effect = lambda by, xpath: (
|
||||
[MagicMock(is_displayed=lambda: True)] * 3
|
||||
if "attachment-indicator" in xpath
|
||||
else [MagicMock(is_displayed=lambda: True)] * 3
|
||||
if "checkbox" in xpath
|
||||
else []
|
||||
)
|
||||
assert not _is_probable_single_task_card(board)
|
||||
|
||||
def test_task_card_matches_title(self):
|
||||
card = _card(text="5.29.26\n1 attachment", attachment_child=True)
|
||||
assert _task_card_matches_title(card, "5.29.26")
|
||||
assert not _task_card_matches_title(card, "Alvarado, Shante")
|
||||
@@ -0,0 +1,99 @@
|
||||
"""Unit tests for task-details panel status detection (scoped to panel, not board)."""
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from selenium.webdriver.common.by import By
|
||||
|
||||
from onedrive_planner import (
|
||||
_panel_progress_status_label,
|
||||
_task_details_shows_in_progress,
|
||||
_task_details_shows_not_started,
|
||||
)
|
||||
|
||||
|
||||
def _panel(*, status_aria: str):
|
||||
panel = MagicMock()
|
||||
panel.is_displayed.return_value = True
|
||||
status_btn = MagicMock()
|
||||
status_btn.is_displayed.return_value = True
|
||||
status_btn.get_attribute.side_effect = lambda name: (
|
||||
status_aria if name == "aria-label" else ""
|
||||
)
|
||||
title_box = MagicMock()
|
||||
title_box.is_displayed.return_value = True
|
||||
title_box.text = "6.5.26"
|
||||
|
||||
def find_elements(by, xpath):
|
||||
xp = xpath or ""
|
||||
if "Not started" in xp or "In progress" in xp or "Completed" in xp:
|
||||
return [status_btn]
|
||||
if "textbox" in xp:
|
||||
return [title_box]
|
||||
if "Status" in xp:
|
||||
return []
|
||||
return []
|
||||
|
||||
panel.find_elements.side_effect = find_elements
|
||||
return panel
|
||||
|
||||
|
||||
def _driver_with_panel(panel, *, board_in_progress: bool = False):
|
||||
drv = MagicMock()
|
||||
board_btn = MagicMock()
|
||||
board_btn.is_displayed.return_value = board_in_progress
|
||||
board_btn.get_attribute.side_effect = lambda name: (
|
||||
"In progress" if name == "aria-label" else ""
|
||||
)
|
||||
|
||||
def find_elements(by, xpath):
|
||||
xp = xpath or ""
|
||||
if "@role='dialog'" in xp or "TaskDetails" in xp or "Close task" in xp:
|
||||
return [panel]
|
||||
if "In progress" in xp and xp.startswith("//button"):
|
||||
return [board_btn] if board_in_progress else []
|
||||
if "Close" in xp:
|
||||
return []
|
||||
return []
|
||||
|
||||
drv.find_elements.side_effect = find_elements
|
||||
return drv
|
||||
|
||||
|
||||
class TestTaskPanelStatus:
|
||||
def test_panel_not_started(self):
|
||||
panel = _panel(status_aria="Not started")
|
||||
assert _panel_progress_status_label(panel) == "not started"
|
||||
|
||||
def test_panel_in_progress(self):
|
||||
panel = _panel(status_aria="In progress")
|
||||
assert _panel_progress_status_label(panel) == "in progress"
|
||||
|
||||
def test_details_not_started_ignores_board_buttons(self):
|
||||
panel = _panel(status_aria="Not started")
|
||||
drv = _driver_with_panel(panel, board_in_progress=True)
|
||||
assert _task_details_shows_not_started(drv)
|
||||
assert not _task_details_shows_in_progress(drv)
|
||||
|
||||
def test_details_in_progress_from_panel_only(self):
|
||||
panel = _panel(status_aria="In progress")
|
||||
drv = _driver_with_panel(panel, board_in_progress=False)
|
||||
assert _task_details_shows_in_progress(drv)
|
||||
assert not _task_details_shows_not_started(drv)
|
||||
|
||||
def test_panel_in_progress_from_combobox_value(self):
|
||||
panel = MagicMock()
|
||||
panel.is_displayed.return_value = True
|
||||
combo = MagicMock()
|
||||
combo.is_displayed.return_value = True
|
||||
combo.get_attribute.side_effect = lambda name: "inProgress" if name == "value" else ""
|
||||
combo.text = ""
|
||||
|
||||
def find_elements(by, xpath):
|
||||
xp = xpath or ""
|
||||
if "combobox" in xp or "ComboBox" in xp:
|
||||
return [combo]
|
||||
return []
|
||||
|
||||
panel.find_elements.side_effect = find_elements
|
||||
assert _panel_progress_status_label(panel) == "in progress"
|
||||
@@ -0,0 +1,29 @@
|
||||
"""Unit tests for TOTP secret sanitization."""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
SRC = Path(__file__).resolve().parent.parent / "src"
|
||||
sys.path.insert(0, str(SRC))
|
||||
|
||||
from totp_utils import sanitize_totp_secret, validate_totp_secret # noqa: E402
|
||||
|
||||
|
||||
def test_sanitize_strips_separators() -> None:
|
||||
assert sanitize_totp_secret("jbsw y3dp-ehpk3pxp") == "JBSWY3DPEHPK3PXP"
|
||||
|
||||
|
||||
def test_sanitize_maps_zero_one() -> None:
|
||||
assert sanitize_totp_secret("JBSW03DP") == "JBSWO3DP"
|
||||
|
||||
|
||||
def test_validate_rejects_invalid_chars() -> None:
|
||||
with pytest.raises(ValueError, match="not valid Base32"):
|
||||
validate_totp_secret("not-valid-secret!")
|
||||
|
||||
|
||||
def test_validate_accepts_standard_secret() -> None:
|
||||
assert validate_totp_secret("JBSWY3DPEHPK3PXP") == "JBSWY3DPEHPK3PXP"
|
||||
@@ -0,0 +1,149 @@
|
||||
"""UiPath migration helper tests and pilot output structure checks."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
UIPATH = ROOT / "uipath" / "MISF_Planner_Download"
|
||||
HELPERS_CS = UIPATH / "Helpers"
|
||||
|
||||
|
||||
def _read_cs(name: str) -> str:
|
||||
return (HELPERS_CS / name).read_text(encoding="utf-8")
|
||||
|
||||
|
||||
class TestUiPathProjectStructure:
|
||||
def test_project_json_exists(self):
|
||||
assert (UIPATH / "project.json").is_file()
|
||||
|
||||
def test_main_workflows_exist(self):
|
||||
for name in (
|
||||
"Main.xaml",
|
||||
"01_ReadConfig.xaml",
|
||||
"02_LoginPlanner.xaml",
|
||||
"03_ProcessFacility.xaml",
|
||||
"04_DownloadTaskAttachments.xaml",
|
||||
"05_SetTaskInProgress.xaml",
|
||||
"06_WriteSummary.xaml",
|
||||
):
|
||||
assert (UIPATH / name).is_file(), f"missing {name}"
|
||||
|
||||
def test_helpers_exist(self):
|
||||
assert (UIPATH / "Helpers" / "GenerateTotp.xaml").is_file()
|
||||
assert (UIPATH / "Helpers" / "SanitizeFolderName.xaml").is_file()
|
||||
assert (UIPATH / "Helpers" / "TotpGenerator.cs").is_file()
|
||||
|
||||
def test_selectors_documented(self):
|
||||
assert (UIPATH / "Selectors" / "PlannerSelectors.json").is_file()
|
||||
assert (UIPATH / "Selectors" / "IFRAME_SETUP.md").is_file()
|
||||
data = json.loads((UIPATH / "Selectors" / "PlannerSelectors.json").read_text())
|
||||
assert "planner_iframe" in data
|
||||
assert "my_plans" in data["planner_iframe"]
|
||||
|
||||
|
||||
class TestHelperLogicMirrorsPython:
|
||||
def test_sanitize_folder_name(self):
|
||||
src = _read_cs("MisfHelpers.cs")
|
||||
assert "SanitizeFolderName" in src
|
||||
assert "InvalidFolderChars" in src
|
||||
|
||||
def test_totp_sanitize_matches_python(self):
|
||||
src = _read_cs("MisfHelpers.cs")
|
||||
assert 'Replace("0", "O")' in src
|
||||
assert 'Replace("1", "I")' in src
|
||||
|
||||
def test_download_url_transform(self):
|
||||
src = _read_cs("MisfHelpers.cs")
|
||||
assert "web=1" in src
|
||||
assert "download=1" in src
|
||||
|
||||
def test_default_planner_url(self):
|
||||
src = _read_cs("MisfConstants.cs")
|
||||
assert "planner.cloud.microsoft" in src
|
||||
assert "0c15ad2c-19dd-4ded-9cd1-20d344386414" in src
|
||||
|
||||
|
||||
class TestPilotOutputLayout:
|
||||
"""Compare output folder layout with Python bot expectations."""
|
||||
|
||||
@pytest.fixture
|
||||
def today_iso(self) -> str:
|
||||
return datetime.now().strftime("%Y-%m-%d")
|
||||
|
||||
def test_output_directories_can_be_created(self, today_iso: str, tmp_path: Path):
|
||||
original = tmp_path / "Output" / today_iso / "Original" / "Test_Facility"
|
||||
workable = tmp_path / "Output" / today_iso / "Workable"
|
||||
summary = tmp_path / "Output" / today_iso / "Summary"
|
||||
for d in (original, workable, summary):
|
||||
d.mkdir(parents=True, exist_ok=True)
|
||||
assert original.is_dir()
|
||||
assert workable.is_dir()
|
||||
assert summary.is_dir()
|
||||
|
||||
def test_checkpoint_json_shape(self, tmp_path: Path):
|
||||
ckpt = {
|
||||
"last_completed_index": 2,
|
||||
"last_facility": "Amaya",
|
||||
"completed_facilities": ["Amaya", "Brighton Place San Diego"],
|
||||
}
|
||||
path = tmp_path / "checkpoint.json"
|
||||
path.write_text(json.dumps(ckpt), encoding="utf-8")
|
||||
loaded = json.loads(path.read_text())
|
||||
assert loaded["last_completed_index"] == 2
|
||||
assert isinstance(loaded["completed_facilities"], list)
|
||||
|
||||
|
||||
class TestMainXamlExpressions:
|
||||
def test_main_has_facility_limit_argument(self):
|
||||
main = (UIPATH / "Main.xaml").read_text(encoding="utf-8")
|
||||
assert "in_FacilityLimit" in main
|
||||
assert "FilterFacilities" in main
|
||||
|
||||
def test_login_invokes_generate_totp(self):
|
||||
login = (UIPATH / "02_LoginPlanner.xaml").read_text(encoding="utf-8")
|
||||
assert "GenerateTotp.xaml" in login
|
||||
assert "loginfmt" in login
|
||||
|
||||
def test_iframe_prefix_in_process_facility(self):
|
||||
proc = (UIPATH / "03_ProcessFacility.xaml").read_text(encoding="utf-8")
|
||||
assert "IFRAME" in proc
|
||||
assert "My plans" in proc
|
||||
|
||||
def test_attachments_tab_download_flow(self):
|
||||
py = (ROOT / "src" / "onedrive_planner.py").read_text(encoding="utf-8")
|
||||
assert "_open_attachments_view" in py
|
||||
assert "_ensure_board_scan_context" in py
|
||||
assert "attachment-indicator" in py
|
||||
assert "_card_has_attachment" in py
|
||||
assert "_task_is_not_started" in py
|
||||
assert "_open_task_via_attachment" in py
|
||||
assert "_click_exact_attachment_name" in py
|
||||
assert "_collect_attachment_hrefs" in py
|
||||
assert "_collect_attachment_download_urls" in py
|
||||
assert "_download_href_via_http" in py
|
||||
assert "_download_attachment_row" in py
|
||||
assert "_list_attachment_rows" in py
|
||||
assert "_close_extra_download_tabs" in py
|
||||
assert "_download_href_with_fallback" in py
|
||||
assert "_download_one_href_browser_new_tab" in py
|
||||
assert "HTTP only last" in py or "HTTP last" in py
|
||||
assert "configure_http_download_proxy" in py
|
||||
assert "_url_from_attachment_element" in py
|
||||
assert "_scroll_attachments_panel" in py
|
||||
dl = (UIPATH / "04_DownloadTaskAttachments.xaml").read_text(encoding="utf-8")
|
||||
assert "Attachments" in dl
|
||||
assert "sharepoint" in dl.lower()
|
||||
|
||||
def test_in_progress_task_details_only(self):
|
||||
proc = (UIPATH / "05_SetTaskInProgress.xaml").read_text(encoding="utf-8")
|
||||
assert "Task details" in proc
|
||||
assert "In progress" in proc
|
||||
py = (ROOT / "src" / "onedrive_planner.py").read_text(encoding="utf-8")
|
||||
assert "_set_task_in_progress_from_task_details" in py
|
||||
assert "_mark_task_in_progress" in py
|
||||
assert "task details only" in py
|
||||
assert "_set_task_in_progress_from_board(driver, card" not in py
|
||||
Reference in new issue
Block a user