Skip to content

Commit cf1ad6c

Browse files
add getting started examples for LLM for Android and iOS
small fixes in android LLM locator logic
1 parent bb0e8e8 commit cf1ad6c

File tree

12 files changed

+245
-33
lines changed

12 files changed

+245
-33
lines changed

src/Bellatrix.Mobile/llm/android/FindByPrompt.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ private string ResolveViaPromptFallback(string location, object context, int max
170170

171171
var result = SemanticKernelService.Kernel.InvokePromptAsync(prompt).Result;
172172
var rawSelector = result?.GetValue<string>()?.Trim();
173+
rawSelector = ParsePromptLocatorToUIAutomator(rawSelector);
173174

174175
if (string.IsNullOrWhiteSpace(rawSelector))
175176
continue;

src/Bellatrix.Mobile/llm/android/skills/AndroidLocatorSkill.cs

Lines changed: 44 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,9 @@ public string BuildLocatorPrompt(string viewSummaryJson, string instruction, Lis
3030
: "(none)";
3131

3232
return $"""
33-
You are an AI assistant helping with Android mobile UI test automation using Appium.
33+
You are an AI assistant for Android UI test automation using Appium.
3434
35-
Your task is to generate a valid, reliable **Android UIAutomator locator** that will scroll to and find the element based on:
35+
Your task is to generate a **valid UiAutomator selector string** for locating an Android element based on:
3636
- A user instruction
3737
- A structured snapshot of visible elements (JSON)
3838
- A list of previously failed selectors
@@ -50,43 +50,51 @@ public string BuildLocatorPrompt(string viewSummaryJson, string instruction, Lis
5050
5151
---
5252
53-
✅ Return a **UiSelector** expression inside **UiScrollable** to scroll to the element:
54-
- textContains("...")
55-
- descriptionContains("...")
56-
- className("...")
57-
- resourceIdMatches(".*value.*")
53+
**UiAutomator Selector Rules (Strict):**
5854
59-
✅ Format Examples:
60-
- uiautomator=new UiScrollable(new UiSelector()).scrollIntoView(new UiSelector().textContains("Login"))
61-
- uiautomator=new UiScrollable(new UiSelector()).scrollIntoView(new UiSelector().descriptionContains("Settings"))
62-
- uiautomator=new UiScrollable(new UiSelector()).scrollIntoView(new UiSelector().resourceIdMatches(".*submit.*"))
63-
- uiautomator=new UiScrollable(new UiSelector()).scrollIntoView(new UiSelector().className("android.widget.Button"))
55+
✅ Return only one of the following as a single line:
56+
- new UiSelector().text("Login")
57+
- new UiSelector().textContains("Settings")
58+
- new UiSelector().description("Save")
59+
- new UiSelector().descriptionContains("Edit")
60+
- new UiSelector().resourceId("com.example:id/button")
61+
- new UiSelector().className("android.widget.CheckBox")
62+
- new UiScrollable(new UiSelector().scrollable(true)).scrollIntoView(new UiSelector().text("Submit"))
63+
64+
✅ If scrolling is needed, use:
65+
- new UiScrollable(new UiSelector().scrollable(true)).scrollIntoView(new UiSelector().text("..."))
6466
6567
🚫 Do NOT use:
68+
- Any `uiautomator=` prefix
69+
- Any `=` between selectors
6670
- XPath locators
67-
- Multiple expressions or fallback options
68-
- Explanations or comments
71+
- Explanations, comments, markdown, or code blocks
72+
- Multiple selectors, only a single valid one
6973
7074
---
7175
72-
**Return Format:**
73-
Only return a single line like:
74-
uiautomator=new UiScrollable(new UiSelector()).scrollIntoView(new UiSelector().textContains("Login"))
76+
**Return Format:**
77+
Return ONLY the UiAutomator selector string as a single line.
78+
For example:
79+
- new UiSelector().text("Login")
80+
- new UiScrollable(new UiSelector().scrollable(true)).scrollIntoView(new UiSelector().className("android.widget.CheckBox"))
7581
76-
Do not include anything else.
82+
Return nothing else.
7783
""";
7884
}
7985

8086
[KernelFunction]
8187
public string HealBrokenLocator(string failedLocator, string oldSnapshot, string newSnapshot)
8288
{
8389
return $"""
84-
You are an AI assistant helping to fix broken Android locators for Appium tests.
90+
You are an AI assistant for fixing broken Android UI locators for Appium.
8591
86-
The original locator failed:
92+
The original UiAutomator locator failed:
8793
❌ Failed: {failedLocator}
8894
89-
Your job is to return a **new valid uiautomator locator** using the updated view snapshot.
95+
Your task is to generate a **new valid UiAutomator selector string** based on:
96+
- A previously working view summary (JSON)
97+
- A new view snapshot after the failure
9098
9199
---
92100
@@ -98,17 +106,24 @@ public string HealBrokenLocator(string failedLocator, string oldSnapshot, string
98106
99107
---
100108
101-
✅ Guidelines:
102-
- Use UiScrollable + UiSelector syntax
103-
- Prefer textContains, descriptionContains, resourceIdMatches
104-
- One clean line, no explanations
109+
**Guidelines:**
105110
106-
---
111+
✅ Use ONLY UiSelector or UiScrollable + UiSelector, e.g.:
112+
- new UiSelector().text("Login")
113+
- new UiScrollable(new UiSelector().scrollable(true)).scrollIntoView(new UiSelector().descriptionContains("Settings"))
107114
108-
**Output Format Example:**
109-
uiautomator=new UiScrollable(new UiSelector()).scrollIntoView(new UiSelector().textContains("Login"))
115+
🚫 Do NOT use:
116+
- Any `uiautomator=` prefix
117+
- XPath selectors
118+
- Comments, explanations, markdown, or code blocks
119+
120+
---
110121
111-
Return ONLY a single locator line.
122+
**Output Format:**
123+
Return ONLY the new UiAutomator selector as a single line, nothing else.
124+
For example:
125+
- new UiSelector().text("Submit")
126+
- new UiScrollable(new UiSelector().scrollable(true)).scrollIntoView(new UiSelector().textContains("Continue"))
112127
""";
113128
}
114129
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
using Bellatrix.Mobile.PageObjects;
2+
using Bellatrix.Mobile.Services.Android;
3+
4+
namespace Bellatrix.Mobile.Android.GettingStarted.LLM;
5+
6+
// 1. All BELLATRIX page objects are implemented as partial classes which means that you have separate files for different parts of it- actions, elements, assertions
7+
// but at the end, they are all built into a single type. This makes the maintainability and readability of these classes much better. Also, you can easier locate what you need.
8+
//
9+
// You can always create BELLATRIX page objects yourself inherit AndroidPage
10+
// We advise you to follow the convention with partial classes, but you are always free to put all pieces in a single file.
11+
public partial class MainAndroidPage : AndroidPage
12+
{
13+
private AndroidKeyboardService _keyboardService;
14+
15+
public MainAndroidPage(AndroidKeyboardService androidKeyboardService) => _keyboardService = androidKeyboardService;
16+
17+
// 2. These elements are always used together when an item is transferred. There are many test cases where you need to transfer different items and so on.
18+
// This way you reuse the code instead of copy-paste it. If there is a change in the way how the item is transferred, change the workflow only here.
19+
// Even single line of code is changed in your tests.
20+
public void TransferItem(string itemToBeTransferred, string userName, string password)
21+
{
22+
_keyboardService.HideKeyboard();
23+
PermanentTransfer.Check();
24+
Items.SelectByText(itemToBeTransferred);
25+
ReturnItemAfter.ToExists().WaitToBe();
26+
UserName.SetText(userName);
27+
Password.SetPassword(password);
28+
KeepMeLogged.Click();
29+
Transfer.Click();
30+
}
31+
32+
protected override string ActivityName => ".view.Controls1";
33+
protected override string PackageName => Constants.AndroidNativeAppId;
34+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
namespace Bellatrix.Mobile.Android.GettingStarted.LLM;
2+
3+
public partial class MainAndroidPage
4+
{
5+
public void AssertPermanentTransferIsChecked()
6+
{
7+
App.Assert.IsTrue(PermanentTransfer.IsChecked);
8+
}
9+
10+
public void AssertRightItemSelected(string itemName)
11+
{
12+
App.Assert.AreEqual(itemName, Items.GetText());
13+
}
14+
15+
public void AssertRightUserNameSet(string userName)
16+
{
17+
App.Assert.AreEqual(userName, UserName.GetText());
18+
}
19+
20+
public void AssertKeepMeLoggedChecked()
21+
{
22+
App.Assert.IsTrue(KeepMeLogged.IsChecked);
23+
}
24+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
namespace Bellatrix.Mobile.Android.GettingStarted.LLM;
2+
3+
public partial class MainAndroidPage
4+
{
5+
// 1. All elements are placed inside the file PageName.Map so that the declarations of your elements to be in a single place.
6+
// It is convenient since if there is a change in some of the locators or elements types you can apply the fix only here.
7+
// All elements are implements as properties. Here we use the short syntax for declaring properties, but you can always use the old one.
8+
// App.Components property is actually a shorter version of ComponentCreateService
9+
public Button Transfer => App.Components.CreateByIdContaining<Button>("button");
10+
public CheckBox PermanentTransfer => App.Components.CreateByIdContaining<CheckBox>("check1");
11+
public ComboBox Items => App.Components.CreateByIdContaining<ComboBox>("spinner1");
12+
public Button ReturnItemAfter => App.Components.CreateByIdContaining<Button>("toggle1");
13+
public Label Results => App.Components.CreateByText<Label>("textColorPrimary");
14+
public Password Password => App.Components.CreateByIdContaining<Password>("edit2");
15+
public TextField UserName => App.Components.CreateByIdContaining<TextField>("edit");
16+
public RadioButton KeepMeLogged => App.Components.CreateByIdContaining<RadioButton>("radio2");
17+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
using NUnit.Framework;
2+
using static Bellatrix.AiValidator;
3+
using static Bellatrix.AiAssert;
4+
5+
namespace Bellatrix.Mobile.Android.GettingStarted.LLM;
6+
7+
[TestFixture]
8+
[Android(Constants.AndroidNativeAppPath,
9+
Constants.AndroidNativeAppId,
10+
Constants.AndroidDefaultAndroidVersion,
11+
Constants.AndroidDefaultDeviceName,
12+
".view.Controls1",
13+
Lifecycle.RestartEveryTime)]
14+
public class PageObjectsTests : NUnit.AndroidTest
15+
{
16+
[Test]
17+
public void ActionsWithoutPageObjects_LLM()
18+
{
19+
var button = App.Components.CreateByIdContaining<Button>("button_disabled");
20+
button.ValidateIsDisabled();
21+
//var checkBox = App.Components.CreateByIdContaining<CheckBox>("check1");
22+
var checkBox = App.Components.CreateByPrompt<CheckBox>("find the checkbox under the disabled button");
23+
checkBox.Check();
24+
checkBox.ValidateIsChecked();
25+
var comboBox = App.Components.CreateByIdContaining<ComboBox>("spinner1");
26+
comboBox.SelectByText("Jupiter");
27+
comboBox.ValidateTextIs("Jupiter");
28+
29+
ValidateByPrompt("validate that the spinner combobox has text Jupiter");
30+
31+
var label = App.Components.CreateByText<Label>("textColorPrimary");
32+
label.ValidateIsVisible();
33+
var radioButton = App.Components.CreateByIdContaining<RadioButton>("radio2");
34+
radioButton.Click();
35+
36+
radioButton.ValidateIsChecked(timeout: 30, sleepInterval: 2);
37+
38+
// fail on purpose to show how smart AI analysis works
39+
//radioButton.ValidateIsNotChecked(timeout: 30, sleepInterval: 2);
40+
}
41+
}

templates/Bellatrix.Android.GettingStarted/testFrameworkSettings.Debug.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,12 +120,12 @@
120120
"qdrantMemoryDbEndpoint": "http://localhost:6333",
121121
"localCacheConnectionString": "env_LocalCacheConnectionString",
122122
"localCacheProjectName": "android_getting_started",
123-
"shouldIndexPageObjects": false,
123+
"shouldIndexPageObjects": true,
124124
"pageObjectFilesPath": "32. Prompts Support\\Pages",
125125
"memoryIndex": "PageObjects",
126126
"resetIndexEverytime": false,
127127
"locatorRetryAttempts": 3,
128-
"validationsTimeout": 5,
128+
"validationsTimeout": 15,
129129
"sleepInterval": 1,
130130
"enableSelfHealing": true,
131131
"enableSmartFailureAnalysis": true
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using Bellatrix.Mobile.PageObjects;
2+
3+
namespace Bellatrix.Mobile.IOS.GettingStarted.LLM;
4+
5+
// 1. All BELLATRIX page objects are implemented as partial classes which means that you have separate files for different parts of it- actions, elements, assertions
6+
// but at the end, they are all built into a single type. This makes the maintainability and readability of these classes much better. Also, you can easier locate what you need.
7+
//
8+
// You can always create BELLATRIX page objects yourself inheriting the IOSPage class
9+
// We advise you to follow the convention with partial classes, but you are always free to put all pieces in a single file.
10+
public partial class CalculatorPage : IOSPage
11+
{
12+
// 2. These elements are always used together when an item is transferred. There are many test cases where you need to transfer different items and so on.
13+
// This way you reuse the code instead of copy-paste it. If there is a change in the way how the item is transferred, change the workflow only here.
14+
// Even single line of code is changed in your tests.
15+
public void Sum(int firstNumber, int secondNumber)
16+
{
17+
NumberOne.SetText(firstNumber.ToString());
18+
NumberTwo.SetText(secondNumber.ToString());
19+
Compute.Click();
20+
}
21+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
using Bellatrix.Mobile.IOS;
2+
3+
namespace Bellatrix.Mobile.IOS.GettingStarted.LLM;
4+
5+
public partial class CalculatorPage
6+
{
7+
public void AssertAnswer(int answer)
8+
{
9+
Answer.ValidateTextIs(answer.ToString());
10+
}
11+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
namespace Bellatrix.Mobile.IOS.GettingStarted.LLM;
2+
3+
public partial class CalculatorPage
4+
{
5+
// 1. All elements are placed inside the file PageName.Map so that the declarations of your elements to be in a single place.
6+
// It is convenient since if there is a change in some of the locators or elements types you can apply the fix only here.
7+
// All elements are implements as properties. Here we use the short syntax for declaring properties, but you can always use the old one.
8+
// App.Components property is actually a shorter version of ComponentCreateService
9+
public Button Compute => App.Components.CreateByName<Button>("ComputeSumButton");
10+
public TextField NumberOne => App.Components.CreateById<TextField>("IntegerA");
11+
public TextField NumberTwo => App.Components.CreateById<TextField>("IntegerB");
12+
public Label Answer => App.Components.CreateByName<Label>("Answer");
13+
}

0 commit comments

Comments
 (0)