diff --git a/Telegram/Tests/Sources/UITests.swift b/Telegram/Tests/Sources/UITests.swift index 7b9083117e..caecf67403 100644 --- a/Telegram/Tests/Sources/UITests.swift +++ b/Telegram/Tests/Sources/UITests.swift @@ -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)) } } diff --git a/docs/ui-testing.md b/docs/ui-testing.md new file mode 100644 index 0000000000..29f275ddb3 --- /dev/null +++ b/docs/ui-testing.md @@ -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. diff --git a/submodules/TelegramUI/Sources/AppDelegate.swift b/submodules/TelegramUI/Sources/AppDelegate.swift index ff39ee10a1..6ab1828272 100644 --- a/submodules/TelegramUI/Sources/AppDelegate.swift +++ b/submodules/TelegramUI/Sources/AppDelegate.swift @@ -646,7 +646,16 @@ private func extractAccountManagerState(records: AccountRecordsView, appLockContext: AppLockContext, notificationController: NotificationContainerController?, applicationBindings: TelegramApplicationBindings, initialPresentationDataAndSettings: InitialPresentationDataAndSettings, networkArguments: NetworkInitializationArguments, hasInAppPurchases: Bool, rootPath: String, legacyBasePath: String?, apsNotificationToken: Signal, voipNotificationToken: Signal, 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, appLockContext: AppLockContext, notificationController: NotificationContainerController?, applicationBindings: TelegramApplicationBindings, initialPresentationDataAndSettings: InitialPresentationDataAndSettings, networkArguments: NetworkInitializationArguments, hasInAppPurchases: Bool, rootPath: String, legacyBasePath: String?, apsNotificationToken: Signal, voipNotificationToken: Signal, 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 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) } })) })