mirror of
https://github.com/TelegramMessenger/Telegram-iOS.git
synced 2026-07-06 03:33:41 +02:00
WIP ui-tests
This commit is contained in:
parent
4864bb1ae4
commit
9988158f83
4 changed files with 139 additions and 14 deletions
|
|
@ -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 it’s 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
120
docs/ui-testing.md
Normal 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.
|
||||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}))
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue