WIP ui-tests

This commit is contained in:
Isaac 2026-02-14 20:01:15 +04:00
parent 4864bb1ae4
commit 9988158f83
4 changed files with 139 additions and 14 deletions

View file

@ -2,22 +2,16 @@ import XCTest
class UITests: XCTestCase {
override func setUpWithError() throws {
// Put setup code here. This method is called before the invocation of each test method in the class.
// In UI tests it is usually best to stop immediately when a failure occurs.
continueAfterFailure = false
// In UI tests its important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
}
override func tearDownWithError() throws {
// Put teardown code here. This method is called after the invocation of each test method in the class.
}
func testLogin() throws {
func testLaunch() throws {
let app = XCUIApplication()
app.launchArguments.append("--ui-test")
app.launch()
XCTAssert(app.buttons["Continue"].waitForExistence(timeout: 5.0))
XCTAssert(app.wait(for: .runningForeground, timeout: 10.0))
}
}

120
docs/ui-testing.md Normal file
View file

@ -0,0 +1,120 @@
# UI Testing
## Running Tests
```bash
xcodebuild test \
-project Telegram/Telegram.xcodeproj \
-scheme iOSAppUITestSuite \
-destination 'platform=iOS Simulator,name=iPhone 17,OS=26.1'
```
Pick any available simulator. List them with `xcrun simctl list devices available iPhone`.
## Test Environment
Tests launch the app with the `--ui-test` argument. When the app detects this flag:
- It uses an isolated data directory (`telegram-ui-tests-data/` inside the app group container), completely separate from production data.
- That directory is wiped on every launch, so each test run starts with a clean slate.
- The app connects to Telegram **test servers** (not production). Test server accounts are independent of production accounts.
This means every test begins with the app in its first-launch state: no accounts, no data, showing the welcome/auth screen.
## Writing Tests
Test files live in `Telegram/Tests/Sources/`. The test target is `iOSAppUITestSuite`.
Tests use Apple's XCUITest framework. Each test class extends `XCTestCase` and interacts with the app through `XCUIApplication`.
### Template
```swift
import XCTest
class MyFeatureTests: XCTestCase {
private var app: XCUIApplication!
override func setUpWithError() throws {
continueAfterFailure = false
app = XCUIApplication()
app.launchArguments.append("--ui-test")
}
override func tearDownWithError() throws {
app = nil
}
func testSomething() throws {
app.launch()
// Find elements
let button = app.buttons["Start Messaging"]
// Wait for elements to appear
XCTAssert(button.waitForExistence(timeout: 5.0))
// Interact
button.tap()
// Assert
XCTAssert(app.textFields["Phone Number"].waitForExistence(timeout: 3.0))
}
}
```
### Key Patterns
**Always pass `--ui-test`** in `launchArguments`. Without it, the app uses production servers and the real database.
**Wait for elements** rather than assuming they exist immediately. Use `element.waitForExistence(timeout:)` before interacting.
**Find elements** by accessibility identifier, label, or type:
```swift
app.buttons["Continue"] // by label
app.textFields["Phone Number"] // by placeholder/label
app.staticTexts["Welcome"] // static text
app.navigationBars["Settings"] // navigation bar
```
**Relaunch between tests** if needed. Each `app.launch()` call with `--ui-test` wipes the database, so every test method that calls `launch()` gets a fresh app state.
**Type text** into fields:
```swift
let field = app.textFields["Phone Number"]
field.tap()
field.typeText("9996621234")
```
### Adding a New Test File
1. Create a new `.swift` file in `Telegram/Tests/Sources/`.
2. The file is automatically picked up by the `iOSAppUITestSuite` target via the Bazel build — no manual target membership changes needed.
3. Run with the same `xcodebuild test` command.
## Telegram Test Servers
The test environment uses 3 separate Telegram datacenters, completely independent from production.
### Test Phone Numbers
Test phone numbers follow the format `99966XYYYY`:
- `X` is the DC number (1, 2, or 3)
- `YYYY` is any random digits
Examples: `9996621234`, `9996710000`, `9996300001`.
### Verification Codes
Test accounts do not receive real SMS. The confirmation code is **the DC number repeated 5 times**:
- DC 1 (`999661YYYY`) -> code `11111`
- DC 2 (`999662YYYY`) -> code `22222`
- DC 3 (`999663YYYY`) -> code `33333`
### Flood Limits
Test numbers are still subject to flood limits. If a number gets rate-limited, pick a different `YYYY` suffix.
### Security
Do not store any important or private information in test accounts. Anyone can use the simplified authorization mechanism to access them.

View file

@ -646,7 +646,16 @@ private func extractAccountManagerState(records: AccountRecordsView<TelegramAcco
LoggingSettings.defaultSettings = LoggingSettings(logToFile: false, logToConsole: false, redactSensitiveData: true)
}
let rootPath = rootPathForBasePath(appGroupUrl.path)
let isUITest = CommandLine.arguments.contains("--ui-test")
let rootPath: String
if isUITest {
let testDataPath = appGroupUrl.path + "/telegram-ui-tests-data"
let _ = try? FileManager.default.removeItem(atPath: testDataPath)
rootPath = rootPathForBasePath(testDataPath)
} else {
rootPath = rootPathForBasePath(appGroupUrl.path)
}
performAppGroupUpgrades(appGroupPath: appGroupUrl.path, rootPath: rootPath)
let deviceSpecificEncryptionParameters = BuildConfig.deviceSpecificEncryptionParameters(rootPath, baseAppBundleId: baseAppBundleId)
@ -1045,7 +1054,7 @@ private func extractAccountManagerState(records: AccountRecordsView<TelegramAcco
self.mainWindow.coveringView = nil
}
}
}, appDelegate: self)
}, appDelegate: self, testingEnvironment: isUITest)
presentationDataPromise.set(sharedContext.presentationData)

View file

@ -181,7 +181,8 @@ public final class SharedAccountContextImpl: SharedAccountContext {
public let locationManager: DeviceLocationManager?
public var callManager: PresentationCallManager?
let hasInAppPurchases: Bool
let testingEnvironment: Bool
private var callStateDisposable: Disposable?
private(set) var currentCallStatusBarNode: CallStatusBarNodeImpl?
@ -283,7 +284,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
private let energyUsageAutomaticDisposable = MetaDisposable()
init(mainWindow: Window1?, sharedContainerPath: String, basePath: String, encryptionParameters: ValueBoxEncryptionParameters, accountManager: AccountManager<TelegramAccountManagerTypes>, appLockContext: AppLockContext, notificationController: NotificationContainerController?, applicationBindings: TelegramApplicationBindings, initialPresentationDataAndSettings: InitialPresentationDataAndSettings, networkArguments: NetworkInitializationArguments, hasInAppPurchases: Bool, rootPath: String, legacyBasePath: String?, apsNotificationToken: Signal<Data?, NoError>, voipNotificationToken: Signal<Data?, NoError>, firebaseSecretStream: Signal<[String: String], NoError>, setNotificationCall: @escaping (PresentationCall?) -> Void, navigateToChat: @escaping (AccountRecordId, PeerId, MessageId?, Bool) -> Void, displayUpgradeProgress: @escaping (Float?) -> Void = { _ in }, appDelegate: AppDelegate?) {
init(mainWindow: Window1?, sharedContainerPath: String, basePath: String, encryptionParameters: ValueBoxEncryptionParameters, accountManager: AccountManager<TelegramAccountManagerTypes>, appLockContext: AppLockContext, notificationController: NotificationContainerController?, applicationBindings: TelegramApplicationBindings, initialPresentationDataAndSettings: InitialPresentationDataAndSettings, networkArguments: NetworkInitializationArguments, hasInAppPurchases: Bool, rootPath: String, legacyBasePath: String?, apsNotificationToken: Signal<Data?, NoError>, voipNotificationToken: Signal<Data?, NoError>, firebaseSecretStream: Signal<[String: String], NoError>, setNotificationCall: @escaping (PresentationCall?) -> Void, navigateToChat: @escaping (AccountRecordId, PeerId, MessageId?, Bool) -> Void, displayUpgradeProgress: @escaping (Float?) -> Void = { _ in }, appDelegate: AppDelegate?, testingEnvironment: Bool = false) {
assert(Queue.mainQueue().isCurrent())
precondition(!testHasInstance)
@ -301,7 +302,8 @@ public final class SharedAccountContextImpl: SharedAccountContext {
self.appLockContext = appLockContext
self.notificationController = notificationController
self.hasInAppPurchases = hasInAppPurchases
self.testingEnvironment = testingEnvironment
self.accountManager.mediaBox.fetchCachedResourceRepresentation = { (resource, representation) -> Signal<CachedMediaResourceRepresentationResult, NoError> in
return fetchCachedSharedResourceRepresentation(accountManager: accountManager, resource: resource, representation: representation)
}
@ -784,7 +786,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
}
if self.activeAccountsValue!.primary == nil && self.activeAccountsValue!.currentAuth == nil {
self.beginNewAuth(testingEnvironment: false)
self.beginNewAuth(testingEnvironment: self.testingEnvironment)
}
}))
})