Global Side Menu Width
Placeholder

אוטומציה של Chrome באנדרואיד

איך לקרוא בדיקת Android Chrome בפרויקט אוטומציה עם Python, Appium ו-Selenium

מסיבות של אבטחת מידע אני לא מציג כאן פרויקט אמיתי מלא, אבל כן אפשר להציג מבנה עבודה נכון.
בדוגמה הזאת מדובר בבדיקת Android Chrome: כלומר אתר Web שנפתח בתוך Chrome של Android Emulator או מכשיר Android אמיתי.

חשוב להבין: למרות שהבדיקה רצה על Android, זו עדיין בדיקת Web. לכן הלוקייטורים ברוב המקרים יהיו
CSS או data-testid, ולא Android ID.
Android ID מתאים יותר לבדיקות Native APK.

הפונקציה הראשית של הטסט

ב-Pytest כל פונקציה שמתחילה ב-test_ נחשבת לטסט. במקרה הזה הפונקציה הראשית היא:

def test_android_chrome_core_actions(android_chrome, by):
    """Teach a longer Appium mobile web flow that can be copied for real websites."""
    # Use the open_mobile_page helper to open the demo website through the Android localhost bridge.
    open_mobile_page(android_chrome, settings.android_base_url)

לפני שהקוד בתוך הפונקציה רץ, Pytest מכין את הפרמטרים שלה:

  • android_chrome – ה-driver של Appium שמחובר ל-Chrome באנדרואיד.
  • by – כלי של Selenium שמגדיר איך מחפשים אלמנטים, למשל לפי CSS.

כלומר הסדר הוא:

1. Pytest finds test_android_chrome_core_actions
2. Pytest creates the android_chrome fixture
3. Appium opens Android Chrome
4. The test function starts running
5. The first action opens the mobile website

פתיחת האתר במובייל

הפעולה הראשונה בתוך הטסט היא פתיחת האתר:

open_mobile_page(android_chrome, settings.android_base_url)

הפונקציה הזאת היא helper function. היא פותחת URL ומצרפת את הכתובת לדוח Allure:

def open_mobile_page(driver, url: str) -> None:
    """Open a mobile website URL and attach the final URL to Allure."""
    # Android Emulator reaches the local demo server through the emulator localhost bridge.
    driver.get(url)
    # The current URL is useful when redirects or mobile routes are involved.
    attach_text("Opened Android Chrome URL", driver.current_url)

ב-Android Emulator בדרך כלל לא פותחים אתר מקומי עם:

http://127.0.0.1:5050

כי בתוך האמולטור 127.0.0.1 מצביע על האמולטור עצמו, ולא על המחשב שמריץ את הבדיקה.
לכן משתמשים בדרך כלל ב:

ANDROID_BASE_URL=http://10.0.2.2:5050

מה זה with allure_step?

אחרי פתיחת האתר מתחילים לחלק את הטסט לצעדים קריאים בדוח Allure.
חשוב: with allure_step(…) זו לא פונקציה חדשה. זה בלוק קוד שמייצר שלב בדוח.

with allure_step("Open mobile page and verify stable landing state"):
    # Verify the brand is visible before starting user actions.
    assert visible(android_chrome, css(by, "brand")).text == "WulfAuto Demo"
    # Verify the hero area exists before tapping into the practice flow.
    visible(android_chrome, css(by, "hero"))
    # Use the assert_text helper to verify the initial status text.
    assert_text(android_chrome, by, "practice_status", "Ready")

כל השורות עם indentation מתחת ל-with שייכות לאותו Allure step.
אם אחת מהן תיכשל, בדוח יהיה ברור שהנפילה קרתה בשלב:

Open mobile page and verify stable landing state

לוקייטורים במקום אחד

כדי לא לפזר selectors בכל הקובץ, אני שומר אותם במילון אחד בשם SEL.
בפרויקט אמיתי זה אחד המקומות העיקריים שמשנים.

# Keep mobile web selectors in one place so page locator changes are easy to make.
SEL = {
    "brand": "[data-testid='brand']",
    "hero": "[data-testid='hero']",
    "hero_cta": "[data-testid='hero-cta']",
    "practice_status": "[data-testid='practice-status']",
    "name_input": "[data-testid='name-input']",
    "email_input": "[data-testid='email-input']",
    "plan_select": "[data-testid='plan-select']",
    "terms_checkbox": "[data-testid='terms-checkbox']",
    "submit_order": "[data-testid='submit-order']",
    "form_result": "[data-testid='form-result']",
}

ואז במקום לכתוב selector ארוך בכל מקום, משתמשים במפתח קצר:

tap(android_chrome, by, "hero_cta")
type_text(android_chrome, by, "email_input", TEST_USER["email"])
assert_text(android_chrome, by, "form_result", "Order saved")

פונקציית css

הפונקציה הזאת מחזירה tuple עבור Selenium. כלומר זוג ערכים:
איך לחפש, ומה לחפש.

def css(by, key: str) -> tuple[str, str]:
    """Return a CSS locator from the selector dictionary."""
    # Android Chrome is still a web browser, so CSS selectors work like desktop Selenium.
    return (by.CSS_SELECTOR, SEL[key])

לדוגמה:

css(by, "email_input")

מחזיר בפועל משהו בסגנון:

(by.CSS_SELECTOR, "[data-testid='email-input']")

נתוני בדיקה במקום אחד

גם נתוני הבדיקה נמצאים למעלה, כדי לא לערבב בין data לבין flow.

# Keep editable mobile test data near the top so it is easy to reuse this file.
TEST_USER = {
    "name": "Android Chrome",
    "email": "[email protected]",
    "plan": "Pro",
}

בפרויקט אחר בדרך כלל מחליפים כאן שם, אימייל, סיסמה, טלפון, כתובת או כל נתון עסקי אחר.

Tap במקום click

במובייל אני מעדיף לקרוא לפעולת לחיצה בשם tap, כי זה מתאר טוב יותר פעולה במכשיר נייד.
בפועל זה עדיין משתמש ב-Selenium click אחרי המתנה שהאלמנט יהיה לחיץ.

def tap(driver, by, key: str, timeout: int = 10) -> None:
    """Tap a mobile web element by selector key after waiting for it to be clickable."""
    # Always wait before tapping because mobile pages can shift while loading.
    clickable(driver, css(by, key), timeout=timeout).click()

שימוש בתוך הטסט:

with allure_step("Start practice with a mobile tap"):
    # Use the tap helper to wait for the CTA and tap it.
    tap(android_chrome, by, "hero_cta")
    # Use the assert_text helper to confirm the tap changed the page state.
    assert_text(android_chrome, by, "practice_status", "Practice started")

הכנסת טקסט לשדות

ב-Mobile Web חשוב קודם לוודא שהשדה נראה, ללחוץ עליו, לנקות טקסט ישן, ואז להכניס ערך חדש.

def type_text(driver, by, key: str, value: str, clear_first: bool = True, timeout: int = 10) -> None:
    """Type text into a mobile web input by selector key."""
    # Wait until the input is visible before interacting with it.
    field = visible(driver, css(by, key), timeout=timeout)
    # Tap the field first so Android Chrome focuses it.
    field.click()
    # Clear old text when a persistent Chrome profile is reused.
    if clear_first:
        field.clear()
    # send_keys types through the active mobile browser field.
    field.send_keys(value)

שימוש:

type_text(android_chrome, by, "name_input", TEST_USER["name"])
type_text(android_chrome, by, "email_input", TEST_USER["email"])

סגירת מקלדת במובייל

אחת הבעיות הנפוצות במובייל היא שהמקלדת מסתירה כפתורים, checkbox או dropdown.
לכן אחרי הכנסת טקסט כדאי לסגור אותה.

def hide_keyboard_safely(driver) -> None:
    """Hide the Android keyboard when it is open and ignore it when it is already closed."""
    # Mobile keyboards often cover dropdowns, checkboxes, and submit buttons.
    try:
        driver.hide_keyboard()
    except Exception:
        # Some devices raise an error when the keyboard is already hidden.
        pass

שימוש:

hide_keyboard_safely(android_chrome)

בחירה מתוך dropdown

אם באתר יש dropdown אמיתי של HTML מסוג <select>,
אפשר להשתמש ב-Select של Selenium.

def select_visible_text(driver, by, key: str, text: str) -> None:
    """Select an option from a real HTML <select> element in Android Chrome."""
    # Hide the keyboard first so the native dropdown is not covered.
    hide_keyboard_safely(driver)
    # Selenium Select should be used only for real HTML select tags.
    Select(visible(driver, css(by, key))).select_by_visible_text(text)

שימוש:

select_visible_text(android_chrome, by, "plan_select", TEST_USER["plan"])

Checkbox בצורה יציבה

לא כדאי פשוט ללחוץ על checkbox בלי לבדוק מה המצב שלו.
אם הוא כבר מסומן ולוחצים עליו שוב, הוא יהפוך ללא מסומן.
לכן הפונקציה הזאת בודקת קודם את המצב הנוכחי.

def set_checkbox(driver, by, key: str, expected_checked: bool) -> None:
    """Set a mobile web checkbox to the requested state without double-tapping it."""
    # Scroll to the checkbox before tapping because mobile viewports are short.
    checkbox = scroll_to_key(driver, by, key)
    # Compare current state to desired state so the action is idempotent.
    if checkbox.is_selected() != expected_checked:
        checkbox.click()

שימוש:

set_checkbox(android_chrome, by, "terms_checkbox", True)

Scroll לאלמנט נמוך במסך

במובייל המסך קצר, ולכן צריך הרבה פעמים לגלול לפני לחיצה או בדיקה.
בדוגמה הזאת מדובר בתוכן Web בתוך Chrome, ולכן JavaScript scroll עובד טוב.

def scroll_to(driver, element) -> None:
    """Scroll a mobile web element into the center of the viewport."""
    # JavaScript scroll is stable for web content inside Android Chrome.
    driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", element)


def scroll_to_key(driver, by, key: str):
    """Find an element by selector key, scroll to it, and return it."""
    # Wait for the element before scrolling to avoid stale or missing elements.
    element = visible(driver, css(by, key))
    # Move the element into a stable visible position.
    scroll_to(driver, element)
    # Return the element so the caller can assert text or tap it.
    return element

שימוש:

deep_card = scroll_to_key(android_chrome, by, "deep_card")
assert "Deep scroll target" in deep_card.text

דוגמה ל-flow מלא

עכשיו אפשר לראות איך כל החלקים מתחברים ל-flow עסקי ברור.

def test_android_chrome_core_actions(android_chrome, by):
    """Teach a longer Appium mobile web flow that can be copied for real websites."""
    # Use the open_mobile_page helper to open the demo website through the Android localhost bridge.
    open_mobile_page(android_chrome, settings.android_base_url)

    # Start an Allure step for initial mobile page checks.
    with allure_step("Open mobile page and verify stable landing state"):
        # Verify the brand is visible before starting user actions.
        assert visible(android_chrome, css(by, "brand")).text == "WulfAuto Demo"
        # Verify the hero area exists before tapping into the practice flow.
        visible(android_chrome, css(by, "hero"))
        # Use the assert_text helper to verify the initial status text.
        assert_text(android_chrome, by, "practice_status", "Ready")

    # Start an Allure step for the first mobile web tap.
    with allure_step("Start practice with a mobile tap"):
        # Use the tap helper to wait for the CTA and tap it.
        tap(android_chrome, by, "hero_cta")
        # Use the assert_text helper to confirm the tap changed the page state.
        assert_text(android_chrome, by, "practice_status", "Practice started")

    # Start an Allure step for filling and submitting a mobile web form.
    with allure_step("Fill mobile form fields and submit order"):
        # Use the type_text helper to focus, clear, and type the user's display name.
        type_text(android_chrome, by, "name_input", TEST_USER["name"])
        # Use the type_text helper to focus, clear, and type the user's email.
        type_text(android_chrome, by, "email_input", TEST_USER["email"])
        # Use the hide_keyboard_safely helper before interacting with lower controls.
        hide_keyboard_safely(android_chrome)
        # Use the select_visible_text helper for a real HTML select dropdown.
        select_visible_text(android_chrome, by, "plan_select", TEST_USER["plan"])
        # Use the set_checkbox helper to scroll and set the checkbox safely.
        set_checkbox(android_chrome, by, "terms_checkbox", True)
        # Use the scroll_to_key helper to bring the submit button into the mobile viewport.
        submit = scroll_to_key(android_chrome, by, "submit_order")
        # Tap submit after it is visible.
        submit.click()
        # Use the assert_text helper to verify the form result.
        assert_text(android_chrome, by, "form_result", "Order saved")

    # Start an Allure step for mobile scroll behavior.
    with allure_step("Scroll to deep mobile content and verify lower page state"):
        # Use the scroll_to_key helper to move to the deepest important card.
        deep_card = scroll_to_key(android_chrome, by, "deep_card")
        # Assert verifies that the expected card is now visible/readable.
        assert "Deep scroll target" in deep_card.text

צירוף מצב המסך לדוח Allure

בסוף הטסט אפשר לצרף לדוח Allure טקסט קצר שמראה מה היה מצב המסך.
זה עוזר להבין מה קרה גם בלי לפתוח וידאו או screenshot.

def attach_mobile_state(driver, by) -> None:
    """Attach useful mobile page state to Allure for debugging failed flows."""
    # Read status text from the page.
    status = visible(driver, css(by, "practice_status")).text
    # Read result text from the form area.
    result = visible(driver, css(by, "form_result")).text
    # Read the active tab panel text.
    tab_panel = visible(driver, css(by, "tab_panel")).text
    # Attach a compact mobile state summary to the Allure report.
    attach_text("Android Chrome page state", f"status={status}\nresult={result}\ntab_panel={tab_panel}")

מה בדרך כלל משנים בפרויקט אחר?

  • משנים את ANDROID_BASE_URL לכתובת האתר החדש.
  • מחליפים את הערכים בתוך TEST_USER.
  • מחליפים את הלוקייטורים בתוך SEL.
  • משנים את שמות ה-allure_step לפי המסע העסקי האמיתי.
  • משאירים את פונקציות העזר, כי הן הופכות את הקוד לקריא וקל לתחזוקה.

פקודת הרצה

לפני ההרצה צריך Android Emulator פתוח ו-Appium server פעיל.

appium --base-path / --allow-insecure chromedriver_autodownload

ואז מריצים את הטסט מתוך תיקיית הפרויקט:

python -m pytest -q tests/android_chrome/test_android_chrome.py -s

לסיכום: המבנה הנכון בעיניי הוא לחלק את הקובץ לארבעה חלקים ברורים:
נתוני בדיקה, לוקייטורים, פונקציות עזר ו-flow עסקי. כך אפשר לקחת את אותו template
ולהתאים אותו במהירות לאתר אחר שרץ ב-Android Chrome.