Locator'lar ve Page Object'ler
Locator'lar
screen() locator döndürür. Locator tembel bir sorgudur: oluşturmak cihaza dokunmaz; her eylem veya okuma onu taze bir görünüm ağacı üzerinde değerlendirir.
| Metot | Eşleştiği |
|---|---|
getByRole(MobileRole.BUTTON), getByRole(role, "name") | rolün iki platformdaki native sınıfları; name etiket, yoksa metindir |
getByText("Save"), getByText("sav", false), getByText(Pattern) | metin, yoksa etiket, yoksa değer (tam, büyük/küçük harf duyarsız alt dize veya regex) |
getByLabel(...) | erişilebilirlik etiketi |
getByTestId("login") | iOS accessibility identifier veya Android resource-id (login, com.app:id/login ile de eşleşir) |
getByPlaceholder(...) | placeholder metni |
getByType("android.widget.Switch") | ham native sınıf |
locate("role=button;name=Sign in") | bir seçici dizesi |
first(), last(), nth(i), within(container), or(other), and(other), filterHasText(...), filterHasNotText(...), filterHas(locator), filterHasNot(locator) ve withTimeout(Duration) ile daraltın.
Koddan locator
MobileLocator'da Appium ve web Locator adlarıyla statik fabrikalar vardır: id(...), accessibilityId(...), xpath(...), iosClassChain(...), iosPredicate(...), className(...), byTestId, byText, byLabel, byPlaceholder, byRole(role[, name]) ve of(seçiciDizesi). Her kullanımda o anki testin cihazına bağlanırlar; bu yüzden static final alan paralel koşularda güvenlidir. Açık cihazı olmayan bir thread'de kullanılırsa MobileException verirler.
private static final MobileLocator SIGN_IN = MobileLocator.xpath("//*[@text='Sign in']");
SIGN_IN.tap();
Test id'lerini tercih edin (iOS'ta accessibilityIdentifier, Android'de resource-id): cihaz diliyle değişmezler. Ardından isimli roller, en son metinler gelir.
Seçici dizeleri
@MobileFind ve screen().locate(...) öğe başına bir dize alır:
| Dize | Karşılığı |
|---|---|
LoginScreen.loginButton (= yok) | getByTestId("LoginScreen.loginButton") |
testid=com.app:id/btnLogin | getByTestId("com.app:id/btnLogin") |
text=Sign in | getByText("Sign in") (tam) |
label=Password | getByLabel("Password") |
placeholder=Username | getByPlaceholder("Username") |
type=XCUIElementTypeSwitch | getByType("XCUIElementTypeSwitch") |
role=button / role=button;name=Sign in | getByRole(MobileRole.BUTTON[, "Sign in"]) |
id=com.app:id/btnLogin | testid= ile aynı (Appium id) |
accessibility=loginButton | iOS identifier, name veya label; Android content-desc (Appium accessibilityId) |
xpath=//android.widget.Button[@text='Sign in'] | görünüm ağacı üzerinde XPath 1.0 |
chain=**/XCUIElementTypeCell[`label == 'Orders'`] | iOS class chain (ortak alt küme) |
predicate=label BEGINSWITH 'Sign' | iOS NSPredicate (ortak alt küme) |
XPath, class chain ve predicate, mobilecli'nin döndürdüğü ağaç üzerinde TestFly içinde çalışır; mobilecli'de bu sorgular yoktur. Sınırlar:
- XPath: öğeler
XCUIElementType…(iOS) veya Android sınıf adıyla, birhierarchykökü altında adlandırılır. Öznitelikler:resource-id,content-desc,text,name,label,value,placeholder,type,class,enabled,visible,selected,checked,focused,x,y,width,height. mobilecli etiketsiz kapsayıcıları atar; Appium Inspector'dan kopyalanan tam yollu XPath'ler eşleşmeyebilir, id, metin veya tipe dayalı olanlar eşleşir. - Class chain:
/ile ayrılmış adımlar, her derinlik için**/, bir tip (XCUIElementTypeönekli veya öneksiz) ya da*, ve[`predicate`]veya[n](1 tabanlı, negatif sondan) filtreleri. - Predicate:
==,!=,BEGINSWITH,ENDSWITH,CONTAINS,LIKE,MATCHESve[c]/[d];AND,OR,NOTve parantezle birleşir;name,label,value,type,identifier,text,placeholderValueve durum bayrakları üzerinde.
Bilinmeyen bir anahtar, bilinmeyen bir rol veya boş değer, geçerli biçimleri listeleyen bir MobileLocatorException verir.
Page object'ler
Sayfalar MobilePage'den türer. @MobileFind alanı bir öğenin iOS ve Android seçicisini birlikte tutar; Appium'daki @iOSXCUITFindBy ve @AndroidFindBy çiftine benzer. Alanları private tutun, testlere niyet metotları verin; ekranı değiştiren metot bir sonraki sayfayı döndürür.
public class HomePage extends MobilePage {
@MobileFind(ios = "HomeScreen.loginButton", android = "com.example:id/btnHome")
@WaitFor // new HomePage() görünene kadar bekler
private MobileLocator loginButton;
@MobileFind(ios = "cookieBannerAcceptAllButton")
private MobileLocator cookieAccept;
@Override
protected void onLoad() { // @WaitFor alanları göründükten sonra çalışır
tapIfShown(cookieAccept);
}
public LoginPage openLogin() {
loginButton.tap();
return new LoginPage();
}
}
public class LoginPage extends MobilePage {
@MobileFind(ios = "LoginScreen.nameTextField", android = "com.example:id/etUser")
@WaitFor
private MobileLocator username;
@MobileFind("role=button;name=Sign in") // value: iki platformda aynı seçici
private MobileLocator signIn;
public LoginPage signIn(String user) { username.fill(user); signIn.tap(); return this; }
public void shouldBeClosed() { expect(username).toBeHidden(); }
}
new HomePage().openLogin().signIn("ada").shouldBeClosed();
new SomePage() sırasıyla şunları yapar:
- testin cihazını alır (
MobilePage(MobileScreen)ekranı açıkça alır, örneğin test runner dışında); - üst sınıflar dahil her
@MobileFindalanına koşan platformun locator'ını atar:ios/android, yoksavalue; - her
@WaitForalanının görünmesinitimeouts.actionMsiçinde bekler; onLoad()çağırır.
Kurallar:
-
@MobileFindalanlarına başlangıç değeri (= null) vermeyin: Java alt sınıf alan başlatıcılarını temel kurucudan sonra çalıştırır ve locator'ı ezer.onLoad()bu başlatıcılardan önce çalışır. -
Koşan platform için seçicisi olmayan alan yine bir locator alır, ama kullanıldığında hata verir ("LoginPage.username: no android selector"). Böylece sayfalar yalnızca bir platformda olan öğeleri tutabilir.
-
Alanlar statik olmayan
MobileLocatorolmalıdır; başka bir tip sayfa oluşturulurken hata verir. -
tapIfShown(locator)şu an görünen bir öğeye dokunur ve kaybolmasını bekler; öğe görünmüyorsa beklemedenfalsedöner. -
Sayfa oluşturmak ekranını bekler. Cucumber'da sayfa örneklerini adımlar arasında step sınıfının alanlarında tutun; ekranı çoktan gitmiş bir sayfayı yeniden oluşturmayın.
-
Seçici dizesi yerine bir alan, Appium'daki
@iOSXCUITFindBy(accessibility = …)gibi platform başına bir tipli öznitelik kullanabilir:iosAccessibility,iosId,iosXpath,iosClassChain,iosPredicate,iosClassName,androidAccessibility,androidId,androidXpath,androidClassName. Bir platform için iki tane vermek (örneğinandroidveandroidId) sayfa oluşturulurken hata verir.@MobileFind(iosAccessibility = "LoginScreen.loginButton", androidId = "com.example:id/btnLogin")
private MobileLocator login; -
Düz sınıflar da çalışır:
page(Type.class)(MobileSupport'tan) veyaMobilePages.init(page, screen),@MobileFindalanlarını bekleme veonLoad()olmadan doldurur.
Örnek sayfalar src/test/java/io/testfly/examples/mobile/HomePage.java ve LoginPage.java dosyalarıdır.
YAML locator dosyaları
Locator'lar bir YAML dosyasında da durabilir; iki platform için tek dosya. Değerler seçici dizeleridir:
# src/test/resources/mobile-locators/login.yml
login:
username:
ios: LoginScreen.nameTextField
android: com.example:id/etUsername
signIn: role=button;name=Sign in # düz değer: iki platform
welcome:
mobile: text=Welcome back # mobile: iki platform
MobileLocators locators = MobileLocators.load("mobile-locators/login.yml"); // önce classpath, yoksa dosya yolu
locators.get("login.username").fill("ada"); // koşan platformun seçicisi
- Gruplar iç içe olabilir; anahtarlar noktayla birleşir (
login.username). Yalnızcaios,androidvemobileanahtarları olan bir map tek bir locator'dır;ios/android,mobile'a göre önceliklidir. - Dosya yüklenirken her seçici kontrol edilir; geçersiz olan, anahtarını belirterek hata verir. Bilinmeyen bir anahtar, anahtarları listeleyerek hata verir.
- Koşan platform için seçicisi olmayan anahtar,
@MobileFind'taki gibi kullanıldığında hata veren bir locator döndürür. get(key)testin cihazını kullanır;get(key, screen)ekranı açıkça alır. Dosyayı bir kez yükleyip paylaşın (örneğin birstatic finalalanda): cihaz durumu tutmaz.- Örnek:
src/test/resources/mobile-locators/login.yml.
Ön koşullar (precondition)
Login gibi ortak hazırlık her testte tekrarlanmak yerine bir ön koşulda durur. Sağlayıcıyı page object'lerle bir MobileConditions alt sınıfında yazın ve @PreCondition ile isteyin:
public class LoginConditions extends MobileConditions {
@ConditionProvider("loggedIn")
public void loggedIn() {
new HomePage().openLogin().signIn(System.getenv("MOBILE_LOGIN_USER"), System.getenv("MOBILE_LOGIN_PASSWORD"));
}
}
@PreCondition("loggedIn")
@Test
public void showsTheBalance() { /* giriş yapılmış olarak başlar */ }
- Sağlayıcıları
src/test/resources/META-INF/services/io.testfly.mobile.MobileConditionsdosyasına (satır başına bir sınıf adı) kaydedin ya da suite'ten önceMobileConditions.register(new LoginConditions())çağırın. - Ne zaman çalışır: uygulama durumu tarayıcı çerezleri gibi saklanamaz.
mobile.app.reset: noneile uygulama durumunu korur, bu yüzden koşul cihaz başına bir kez çalışır;relaunchveyaclearDataile onu isteyen her testten önce çalışır. Yeniden denemede (retry) her zaman tekrar çalışır. - TestNG
@PreCondition'ı test metodundan okur; JUnit 5 metottan, sonra sınıftan. Cucumber okumaz:Backgroundkullanın. - Mobil bir testte
@PreConditionadları yalnızcaMobileConditionssağlayıcılarına bakar; webBaseConditionssağlayıcıları tarayıcı testleri içindir. - Örnek:
src/test/java/io/testfly/examples/mobile/LoginConditions.java.
Eylemler ve doğrulamalar
- Eylemler:
tap(),doubleTap(),longPress(),fill(text)(type(text)ile aynı; önce temizler),append(text),clear(),swipe(SwipeDirection),pinchOut()/pinchIn()(öğenin ortasında yakınlaştırma / uzaklaştırma) vedragTo(target). Tam olarak bir öğe eşleşene ve görünür, etkin ve sabit olana kadar bekler, sonra öğenin ortasında işlem yapar. Birden çok eşleşme,first(),last()veyanth(i)biri seçmedikçe hata verir (strict mode). - Kaydırma:
scrollIntoView()yukarı (veyascrollIntoView(SwipeDirection)yönünde) kaydırır, ta ki tek bir eşleşme görünene kadar; liste sonunda veyatimeouts.actionMsdolunca hata verir. - Okumalar:
getText(),getValue(),getAttribute(name)venode()görünür bir öğeyi bekler;getAttribute,resource-id,content-descveclassadlarını da kabul eder, olmayan öznitelik içinnulldöner.count(),nodes(),allTexts(),isVisible(),isPresent(),isHidden(),isEnabled(),isDisabled(),isSelected(),isChecked()veisFocused()beklemeden o anki ekranı okur;is…durum okumaları birden fazla eşleşmede hata verir (strict).waitForVisible()vewaitForHidden()açıkça bekler. - Doğrulamalar:
expect(locator)iletoBeVisible(),toBeHidden(),toBeEnabled(),toBeDisabled(),toBeSelected(),toBeChecked(),toBeFocused(),toHaveText(...),toContainText(...),toHaveValue(...),toHaveCount(n)venot().timeouts.actionMsbitene kadar yeniden dener. - Cihaz:
device();launchApp,terminateApp,clearAppData,installApp,openUrl(deep link),pressButton("BACK"),screenSize()vescreenshot()sunar;screen().swipe(...)ekran boyunca kaydırır. - Cihaz ayarları:
device().setOrientation(MobileOrientation.LANDSCAPE)veorientation();setLocation(enlem, boylam)veclearLocation(). İkisi de cihaz kapanırken geri alınır, böylece sonraki test eski yönde ve gerçek konumla başlar.startRecording(path)/stopRecording()ekranı elle kaydeder; otomatik kayıt içinrecordVideo.setAnimations(false)vesetAppearance("dark")(yapılandırmadaanimations/appearanceolarak da var). - Pano ve dosyalar:
clipboard(),setClipboard(text),clearClipboard();pushFile(yerelYol, "/sdcard/Download/photo.jpg")vepullFile(uzakYol, yerelYol)(Android ve iOS simülatör, dosya başına en çok 1 MB). - Uygulamalar ve klavye:
isAppInstalled(id),foregroundApp(),uninstallApp(id),backgroundApp(Duration)(ana ekran, bekle, uygulamaya dön);isKeyboardShown()vehideKeyboard()(Android BACK'e basar; iOS klavyedeki Done/Return tuşuna dokunur). - Crash raporları:
crashReports()vecrashReport(id). Bir mobil test düştüğünde, test sırasında yazılan crash raporlarıtarget/reports/crashes/altına kaydedilir ve adım olarak loglanır. - Webview'ler:
device().webViews()öndeki uygulamanın webview'lerini (ve görünen Chrome/Safari sekmelerini) listeler;webView()görünen tek webview'i döndürür.MobileWebView;url(),title(),content(),evaluate(js),query(css),navigate(url),reload(),goBack(),goForward()vewaitForLoad()sunar.
Eylemler rapora adım olarak yazılır ("Tap getByText(…)"); yazılan metin loglanmaz. Bir test başarısız olduğunda cihaz ekran görüntüsü eklenir (screenshotOnFailure).