בדיקת iPhone Safari בפרויקט אוטומציה עם Python, Appium ו-Selenium
מסיבות של אבטחת מידע אני לא מציג כאן פרויקט אמיתי מלא, אבל כן אפשר להציג מבנה עבודה נכון.
בדוגמה הזאת מדובר בבדיקת iPhone Safari: כלומר אתר Web שנפתח בתוך Safari של iPhone Simulator או מכשיר iPhone אמיתי.
חשוב להבין: למרות שהבדיקה רצה על iPhone, זו עדיין בדיקת Web. לכן הלוקייטורים הם בדרך כלל
CSS או data-testid, ולא Accessibility ID של אפליקציה Native.
Accessibility ID מתאים יותר לבדיקות של אפליקציות iOS אמיתיות.
הפונקציה הראשית של הטסט
ב-Pytest כל פונקציה שמתחילה ב-test_ נחשבת לטסט. במקרה הזה הפונקציה הראשית היא:
def test_iphone_safari_core_actions(iphone_safari, by):
"""Teach a longer Appium mobile web flow that can be copied for iPhone Safari websites."""
# Use the open_mobile_page helper to open the demo website in iPhone Safari.
open_mobile_page(iphone_safari, settings.ios_base_url)
# Reset the local demo page because iPhone Safari can preserve WebView state between Appium sessions.
reset_demo_page(iphone_safari)
לפני שהקוד בתוך הפונקציה רץ, Pytest מכין את הפרמטרים שלה:
- iphone_safari – ה-driver של Appium שמחובר ל-Safari באייפון.
- by – כלי של Selenium שמגדיר איך מחפשים אלמנטים, למשל לפי CSS.
כלומר הסדר הוא:
1. Pytest finds test_iphone_safari_core_actions
2. Pytest creates the iphone_safari fixture
3. Appium opens iPhone Safari
4. The test function starts running
5. The first action opens the mobile website
פתיחת האתר ב-iPhone Safari
הפעולה הראשונה בתוך הטסט היא פתיחת האתר:
open_mobile_page(iphone_safari, settings.ios_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."""
# Add a cache-buster so repeated Safari sessions start with a fresh demo page.
separator = "&" if "?" in url else "?"
fresh_url = f"{url}{separator}run={int(time.time() * 1000)}"
# iPhone Simulator usually reaches the local demo site through settings.ios_base_url.
driver.get(fresh_url)
# The current URL is useful when redirects or mobile routes are involved.
attach_text("Opened iPhone Safari URL", driver.current_url)
ב-iPhone Simulator בדרך כלל אפשר לפתוח אתר שרץ על המחשב דרך כתובת רשת של המחשב, למשל:
IOS_BASE_URL=http://your-computer-ip:5050
בפרויקט אמיתי לא כותבים את ה-URL בתוך הטסט עצמו, אלא שומרים אותו בקובץ settings / environment.
כך אפשר להחליף סביבה בלי לשנות את קוד הבדיקה.
למה צריך reset ב-Safari?
Safari יכול לפעמים לשמור מצב ישן של WebView בין הרצות Appium.
לדוגמה, אם בטסט קודם לחצנו על כפתור, בהרצה הבאה הדף עלול להיפתח כשהסטטוס כבר השתנה.
כדי שהדמו יהיה יציב, אפשר לאפס את מצב הדף בתחילת הטסט.
def reset_demo_page(driver) -> None:
"""Reset the local demo page when iPhone Safari keeps old WebView state."""
# This reset is specific to the local demo website and keeps repeated Safari runs deterministic.
driver.execute_script(
"""
const status = document.querySelector("[data-testid='practice-status']");
const form = document.querySelector("[data-testid='order-form']");
const result = document.querySelector("[data-testid='form-result']");
const panel = document.querySelector("[data-testid='tab-panel']");
const asyncResult = document.querySelector("[data-testid='async-result']");
const summaryTab = document.querySelector("[data-testid='tab-summary']");
document.querySelectorAll(".tab").forEach((tab) => tab.classList.remove("active"));
if (status) status.textContent = "Ready";
if (form) form.reset();
if (result) result.textContent = "";
if (panel) panel.textContent = "Summary: stable text for a simple assertion.";
if (asyncResult) asyncResult.textContent = "";
if (summaryTab) summaryTab.classList.add("active");
window.scrollTo(0, 0);
"""
)
חשוב: זה מתאים לדמו מקומי שאנחנו שולטים בו. באתר אמיתי לא תמיד מאפסים את הדף עם JavaScript.
באתר אמיתי בדרך כלל מגיעים למצב התחלתי דרך logout, ניקוי נתונים, API setup או משתמש בדיקה חדש.
מה זה with allure_step?
אחרי פתיחת האתר מתחילים לחלק את הטסט לצעדים קריאים בדוח Allure.
with allure_step(…) זו לא פונקציה חדשה, אלא בלוק קוד שמייצג שלב בדוח.
with allure_step("Open mobile Safari page and verify stable landing state"):
# Verify the brand is visible before starting user actions.
assert visible(iphone_safari, css(by, "brand")).text == "WulfAuto Demo"
# Verify the hero area exists before tapping into the practice flow.
visible(iphone_safari, css(by, "hero"))
# Use the assert_text helper to verify the initial status text.
assert_text(iphone_safari, by, "practice_status", "Ready")
כל השורות עם indentation מתחת ל-with שייכות לאותו Allure step.
אם אחת מהן תיכשל, בדוח יהיה ברור באיזה שלב עסקי הטסט נפל.
לוקייטורים במקום אחד
כדי לא לפזר 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(iphone_safari, by, "hero_cta")
type_text(iphone_safari, by, "email_input", TEST_USER["email"])
assert_text(iphone_safari, by, "form_result", "Order saved")
פונקציית css
הפונקציה הזאת מחזירה tuple עבור Selenium. כלומר זוג ערכים:
איך לחפש, ומה לחפש.
def css(by, key: str) -> tuple[str, str]:
"""Return a CSS locator from the selector dictionary."""
# iPhone Safari 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 Safari test data near the top so this file is easy to copy.
TEST_USER = {
"name": "iPhone Safari",
"email": "[email protected]",
"plan": "Pro",
}
בפרויקט אחר בדרך כלל מחליפים כאן שם, אימייל, סיסמה, טלפון, כתובת או כל נתון עסקי אחר.
Tap במקום click
במובייל אני מעדיף לקרוא לפעולת לחיצה בשם tap,
כי זה מתאר טוב יותר פעולה במכשיר נייד.
בפועל זה עדיין משתמש ב-Selenium click אחרי המתנה שהאלמנט יהיה לחיץ.
def tap(driver, by, key: str, timeout: int = 10) -> None:
"""Tap a mobile Safari 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 Safari tap"):
# Use the tap helper to wait for the CTA and tap it.
tap(iphone_safari, by, "hero_cta")
# Use the assert_text helper to confirm the tap changed the page state.
assert_text(iphone_safari, 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 Safari 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 iPhone Safari focuses it.
field.click()
# Clear old text when a persistent Safari state is reused.
if clear_first:
field.clear()
# send_keys types through the active mobile browser field.
field.send_keys(value)
שימוש:
type_text(iphone_safari, by, "name_input", TEST_USER["name"])
type_text(iphone_safari, by, "email_input", TEST_USER["email"])
סגירת מקלדת ב-iPhone Safari
במובייל המקלדת יכולה להסתיר dropdown, checkbox או כפתור submit.
לכן אחרי הכנסת טקסט כדאי לסגור אותה בצורה בטוחה.
def hide_keyboard_safely(driver) -> None:
"""Hide the iOS keyboard when it is open and ignore it when it is already closed."""
# iOS Safari keyboards often cover dropdowns, checkboxes, and submit buttons.
try:
driver.hide_keyboard()
except Exception:
# Some iOS sessions raise an error when the keyboard is already hidden.
pass
בחירה מתוך 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 iPhone Safari."""
# Hide the keyboard first so the native picker 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)
Checkbox בצורה יציבה
ב-iPhone Safari לפעמים checkbox קטן לא מקבל click יציב אחרי scroll.
לכן בדמו הזה משתמשים ב-JavaScript click אחרי שהאלמנט כבר נמצא ונגלל למרכז המסך.
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:
# JavaScript click is more reliable for small checkbox controls in iPhone Safari.
driver.execute_script("arguments[0].click();", checkbox)
Scroll לאלמנט נמוך במסך
במובייל המסך קצר, ולכן צריך הרבה פעמים לגלול לפני לחיצה או בדיקה.
בדוגמה הזאת מדובר בתוכן Web בתוך Safari, ולכן 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 iPhone Safari.
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
בדיקת טקסט בצורה יציבה
ב-Safari לפעמים Selenium expected condition סטנדרטי איטי יותר בקריאת טקסט.
לכן אפשר לכתוב helper שקורא את הטקסט בכל poll ובודק אם הטקסט הצפוי נמצא בפנים.
def assert_text(driver, by, key: str, expected_text: str, timeout: int = 10) -> None:
"""Wait until a mobile Safari web element contains expected text."""
# Safari can occasionally be slow to expose text to Selenium expected conditions.
def element_has_text(active_driver):
# Read the current visible text every poll.
current_text = visible(active_driver, css(by, key), timeout=timeout).text
# Return true only when the expected text appears.
return expected_text in current_text
# WebDriverWait keeps the business flow short and readable.
WebDriverWait(driver, timeout).until(element_has_text)
דוגמה ל-flow מלא
def test_iphone_safari_core_actions(iphone_safari, by):
"""Teach a longer Appium mobile web flow that can be copied for iPhone Safari websites."""
# Use the open_mobile_page helper to open the demo website in iPhone Safari.
open_mobile_page(iphone_safari, settings.ios_base_url)
# Reset the local demo page because iPhone Safari can preserve WebView state between Appium sessions.
reset_demo_page(iphone_safari)
# Start an Allure step for initial mobile page checks.
with allure_step("Open mobile Safari page and verify stable landing state"):
# Verify the brand is visible before starting user actions.
assert visible(iphone_safari, css(by, "brand")).text == "WulfAuto Demo"
# Verify the hero area exists before tapping into the practice flow.
visible(iphone_safari, css(by, "hero"))
# Use the assert_text helper to verify the initial status text.
assert_text(iphone_safari, by, "practice_status", "Ready")
# Start an Allure step for the first mobile Safari tap.
with allure_step("Start practice with a Safari tap"):
# Use the tap helper to wait for the CTA and tap it.
tap(iphone_safari, by, "hero_cta")
# Use the assert_text helper to confirm the tap changed the page state.
assert_text(iphone_safari, by, "practice_status", "Practice started")
# Start an Allure step for filling and submitting a mobile Safari form.
with allure_step("Fill mobile Safari form fields and submit order"):
# Use the type_text helper to focus, clear, and type the user's display name.
type_text(iphone_safari, by, "name_input", TEST_USER["name"])
# Use the type_text helper to focus, clear, and type the user's email.
type_text(iphone_safari, by, "email_input", TEST_USER["email"])
# Use the hide_keyboard_safely helper before interacting with lower controls.
hide_keyboard_safely(iphone_safari)
# Use the select_visible_text helper for a real HTML select dropdown.
select_visible_text(iphone_safari, by, "plan_select", TEST_USER["plan"])
# Use the set_checkbox helper to scroll and set the checkbox safely.
set_checkbox(iphone_safari, by, "terms_checkbox", True)
# Use the scroll_to_key helper to bring the submit button into the mobile viewport.
submit = scroll_to_key(iphone_safari, by, "submit_order")
# JavaScript click is stable for submit after Safari mobile scrolling.
iphone_safari.execute_script("arguments[0].click();", submit)
# Use the assert_text helper to verify the form result.
assert_text(iphone_safari, by, "form_result", "Order saved")
מה בדרך כלל משנים בפרויקט אחר?
- משנים את IOS_BASE_URL לכתובת האתר החדש.
- מחליפים את הערכים בתוך TEST_USER.
- מחליפים את הלוקייטורים בתוך SEL.
- משנים את שמות ה-allure_step לפי המסע העסקי האמיתי.
- משאירים את פונקציות העזר, כי הן הופכות את הקוד לקריא וקל לתחזוקה.
פקודת הרצה
לפני ההרצה צריך iPhone Simulator פתוח, Appium server פעיל ואתר שרץ ונגיש מהסימולטור.
appium --base-path /
ואז מריצים את הטסט מתוך תיקיית הפרויקט:
python -m pytest -q tests/iphone_safari/test_iphone_safari.py -s
לסיכום: iPhone Safari הוא Mobile Web, ולכן המבנה דומה ל-Desktop Chrome:
נתונים, selectors, helper functions ו-flow עסקי. ההבדלים המרכזיים הם ניהול keyboard,
מצב WebView שנשמר בין הרצות, ו-scroll/click שדורשים לפעמים התאמה קטנה ל-Safari.