From dec912f2db219aec3bf7d95dcd5c88a15b321ff7 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Sun, 15 Feb 2026 22:50:18 +0400 Subject: [PATCH] Update --- Telegram/Tests/Sources/UITests.swift | 34 +++- build-system/test-helper/Package.resolved | 23 +++ build-system/test-helper/Package.swift | 16 ++ build-system/test-helper/Sources/main.swift | 165 ++++++++++++++++++ docs/ui-testing.md | 16 +- ...ationSequenceCodeEntryControllerNode.swift | 1 + ...rizationSequenceSignUpControllerNode.swift | 10 +- .../CodeInputView/Sources/CodeInputView.swift | 4 +- .../TelegramUI/Sources/AppDelegate.swift | 4 +- 9 files changed, 254 insertions(+), 19 deletions(-) create mode 100644 build-system/test-helper/Package.resolved create mode 100644 build-system/test-helper/Package.swift create mode 100644 build-system/test-helper/Sources/main.swift diff --git a/Telegram/Tests/Sources/UITests.swift b/Telegram/Tests/Sources/UITests.swift index a3085e498f..b58771f356 100644 --- a/Telegram/Tests/Sources/UITests.swift +++ b/Telegram/Tests/Sources/UITests.swift @@ -16,10 +16,9 @@ class UITests: XCTestCase { func testLaunch() throws { app.launch() XCTAssert(app.wait(for: .runningForeground, timeout: 10.0)) - let _ = app.buttons["___non_existing"].waitForExistence(timeout: 10000.0) } - func testLoginToCodeEntry() throws { + func testLoginToSetName() throws { app.launch() // Welcome screen — tap Start Messaging @@ -29,17 +28,16 @@ class UITests: XCTestCase { // Phone entry screen — enter test phone number let countryCodeField = app.textFields["Auth.PhoneEntry.CountryCodeField"] - XCTAssert(countryCodeField.waitForExistence(timeout: 5.0)) + XCTAssert(countryCodeField.waitForExistence(timeout: 10.0)) countryCodeField.tap() - countryCodeField.press(forDuration: 0.5) - if app.menuItems["Select All"].waitForExistence(timeout: 2.0) { - app.menuItems["Select All"].tap() + for _ in 0..<10 { + countryCodeField.typeText(XCUIKeyboardKey.delete.rawValue) } countryCodeField.typeText("999") let phoneNumberField = app.textFields["Auth.PhoneEntry.PhoneNumberField"] phoneNumberField.tap() - phoneNumberField.typeText("6621234") + phoneNumberField.typeText("6625678") let continueButton = app.buttons["Auth.PhoneEntry.ContinueButton"] XCTAssert(continueButton.waitForExistence(timeout: 3.0)) @@ -51,8 +49,26 @@ class UITests: XCTestCase { XCTAssert(confirmButton.waitForExistence(timeout: 5.0)) confirmButton.tap() - // Code entry screen — verify we arrived + // Code entry screen — enter verification code let codeEntryTitle = app.staticTexts["Auth.CodeEntry.Title"] - XCTAssert(codeEntryTitle.waitForExistence(timeout: 10.0)) + XCTAssert(codeEntryTitle.waitForExistence(timeout: 15.0)) + + let codeField = app.textFields["Auth.CodeEntry.CodeField"] + XCTAssert(codeField.waitForExistence(timeout: 3.0)) + codeField.typeText("22222") + + // Set name screen — enter name and submit + let firstNameField = app.textFields["Auth.SetName.FirstNameField"] + XCTAssert(firstNameField.waitForExistence(timeout: 15.0)) + firstNameField.tap() + firstNameField.typeText("Test") + + let lastNameField = app.textFields["Auth.SetName.LastNameField"] + lastNameField.tap() + lastNameField.typeText("User") + + let signUpButton = app.buttons["Auth.SetName.ContinueButton"] + XCTAssert(signUpButton.waitForExistence(timeout: 3.0)) + signUpButton.tap() } } diff --git a/build-system/test-helper/Package.resolved b/build-system/test-helper/Package.resolved new file mode 100644 index 0000000000..b352e4b6a2 --- /dev/null +++ b/build-system/test-helper/Package.resolved @@ -0,0 +1,23 @@ +{ + "pins" : [ + { + "identity" : "tdlibframework", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Swiftgram/TDLibFramework", + "state" : { + "revision" : "fdda50b9335171329237fab2381cbc2e6e3ce86c", + "version" : "1.8.60-cb863c16" + } + }, + { + "identity" : "tdlibkit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Swiftgram/TDLibKit", + "state" : { + "revision" : "245888f853f5e304029f4fa423af14a045476f30", + "version" : "1.5.2-tdlib-1.8.60-cb863c16" + } + } + ], + "version" : 2 +} diff --git a/build-system/test-helper/Package.swift b/build-system/test-helper/Package.swift new file mode 100644 index 0000000000..e8635f1045 --- /dev/null +++ b/build-system/test-helper/Package.swift @@ -0,0 +1,16 @@ +// swift-tools-version: 5.9 +import PackageDescription + +let package = Package( + name: "test-helper", + platforms: [.macOS(.v13)], + dependencies: [ + .package(url: "https://github.com/Swiftgram/TDLibKit", exact: "1.5.2-tdlib-1.8.60-cb863c16"), + ], + targets: [ + .executableTarget( + name: "test-helper", + dependencies: ["TDLibKit"] + ), + ] +) diff --git a/build-system/test-helper/Sources/main.swift b/build-system/test-helper/Sources/main.swift new file mode 100644 index 0000000000..944ee672b1 --- /dev/null +++ b/build-system/test-helper/Sources/main.swift @@ -0,0 +1,165 @@ +import Foundation +import TDLibKit +import TDLibFramework + +// MARK: - Argument parsing + +func printUsage() -> Never { + fputs("Usage: test-helper delete-account --api-id --api-hash --phone \n", stderr) + exit(1) +} + +func parseArgs() -> (apiId: Int, apiHash: String, phone: String) { + let args = CommandLine.arguments + guard args.count >= 2, args[1] == "delete-account" else { printUsage() } + + var apiId: Int? + var apiHash: String? + var phone: String? + + var i = 2 + while i < args.count { + switch args[i] { + case "--api-id": + i += 1; guard i < args.count, let v = Int(args[i]) else { printUsage() } + apiId = v + case "--api-hash": + i += 1; guard i < args.count else { printUsage() } + apiHash = args[i] + case "--phone": + i += 1; guard i < args.count else { printUsage() } + phone = args[i] + default: + printUsage() + } + i += 1 + } + + guard let apiId, let apiHash, let phone else { printUsage() } + return (apiId, apiHash, phone) +} + +// MARK: - Phone number validation + +/// Parses a test phone number (format: 99966XYYYY or +99966XYYYY). +/// Returns (fullNumber with + prefix, verification code). +func parseTestPhone(_ phone: String) -> (fullNumber: String, code: String)? { + let digits = phone.hasPrefix("+") ? String(phone.dropFirst()) : phone + guard digits.count == 10, digits.hasPrefix("99966") else { return nil } + let dcDigit = digits[digits.index(digits.startIndex, offsetBy: 5)] + guard dcDigit >= "1", dcDigit <= "3" else { return nil } + let code = String(repeating: dcDigit, count: 5) + let fullNumber = "+\(digits)" + return (fullNumber, code) +} + +// MARK: - TDLib account deletion + +func deleteTestAccount(apiId: Int, apiHash: String, phone: String, code: String) async throws { + let tmpDir = FileManager.default.temporaryDirectory + .appendingPathComponent("test-helper-\(ProcessInfo.processInfo.processIdentifier)") + try FileManager.default.createDirectory(at: tmpDir, withIntermediateDirectories: true) + defer { try? FileManager.default.removeItem(at: tmpDir) } + + // Suppress TDLib's verbose C++ logging + td_execute("{\"@type\":\"setLogVerbosityLevel\",\"new_verbosity_level\":0}") + + let manager = TDLibClientManager() + defer { manager.closeClients() } + + let authState = AsyncStream.makeStream(of: AuthorizationState.self) + + let client = manager.createClient { data, client in + guard let update = try? client.decoder.decode(Update.self, from: data) else { return } + if case .updateAuthorizationState(let s) = update { + authState.continuation.yield(s.authorizationState) + } + } + + for await state in authState.stream { + switch state { + case .authorizationStateWaitTdlibParameters: + try await client.setTdlibParameters( + apiHash: apiHash, + apiId: apiId, + applicationVersion: "1.0", + databaseDirectory: tmpDir.path, + databaseEncryptionKey: Data(), + deviceModel: "test-helper", + filesDirectory: tmpDir.appendingPathComponent("files").path, + systemLanguageCode: "en", + systemVersion: "macOS", + useChatInfoDatabase: false, + useFileDatabase: false, + useMessageDatabase: false, + useSecretChats: false, + useTestDc: true + ) + + case .authorizationStateWaitPhoneNumber: + try await client.setAuthenticationPhoneNumber( + phoneNumber: phone, + settings: PhoneNumberAuthenticationSettings?.none + ) + + case .authorizationStateWaitCode: + try await client.checkAuthenticationCode(code: code) + + case .authorizationStateWaitRegistration: + print("No account for \(phone) (sign-up requested). Nothing to delete.") + authState.continuation.finish() + return + + case .authorizationStateReady: + print("Logged in. Deleting account...") + try await client.deleteAccount(password: String?.none, reason: "test cleanup") + print("Account deleted.") + authState.continuation.finish() + return + + case .authorizationStateClosed: + authState.continuation.finish() + return + + default: + fputs("Unexpected auth state: \(state)\n", stderr) + authState.continuation.finish() + throw NSError(domain: "test-helper", code: 1, userInfo: [ + NSLocalizedDescriptionKey: "Unexpected auth state" + ]) + } + } +} + +// MARK: - Main + +let (apiId, apiHash, phone) = parseArgs() + +guard let parsed = parseTestPhone(phone) else { + fputs("Error: phone must match format 99966XYYYY (X = 1, 2, or 3)\n", stderr) + exit(1) +} + +do { + try await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + try await deleteTestAccount(apiId: apiId, apiHash: apiHash, phone: parsed.fullNumber, code: parsed.code) + } + group.addTask { + try await Task.sleep(for: .seconds(30)) + throw NSError(domain: "test-helper", code: 1, userInfo: [ + NSLocalizedDescriptionKey: "Timed out after 30 seconds" + ]) + } + try await group.next() + group.cancelAll() + } +} catch let error as TDLibKit.Error { + fputs("Error: [\(error.code)] \(error.message)\n", stderr) + exit(1) +} catch { + fputs("Error: \(error)\n", stderr) + exit(1) +} + +exit(0) diff --git a/docs/ui-testing.md b/docs/ui-testing.md index 29f275ddb3..7c6139af60 100644 --- a/docs/ui-testing.md +++ b/docs/ui-testing.md @@ -96,20 +96,26 @@ field.typeText("9996621234") The test environment uses 3 separate Telegram datacenters, completely independent from production. +### OS Environment + +Test logins are guarded behind a specialized OS environment. The simulator or device must be configured for the test environment before test accounts can authenticate. + ### Test Phone Numbers Test phone numbers follow the format `99966XYYYY`: - `X` is the DC number (1, 2, or 3) -- `YYYY` is any random digits +- `YYYY` are random digits -Examples: `9996621234`, `9996710000`, `9996300001`. +The country code for test numbers is `999`, and the remaining digits are `66XYYYY`. + +Examples: `+999 66 2 1234`, `+999 66 1 0000`, `+999 66 3 0001`. ### 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` +- DC 1 (`+999 661 YYYY`) -> code `11111` +- DC 2 (`+999 662 YYYY`) -> code `22222` +- DC 3 (`+999 663 YYYY`) -> code `33333` ### Flood Limits diff --git a/submodules/AuthorizationUI/Sources/AuthorizationSequenceCodeEntryControllerNode.swift b/submodules/AuthorizationUI/Sources/AuthorizationSequenceCodeEntryControllerNode.swift index ad036e9bb3..26b4c2a57a 100644 --- a/submodules/AuthorizationUI/Sources/AuthorizationSequenceCodeEntryControllerNode.swift +++ b/submodules/AuthorizationUI/Sources/AuthorizationSequenceCodeEntryControllerNode.swift @@ -179,6 +179,7 @@ final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextF self.codeInputView.textField.keyboardAppearance = self.theme.rootController.keyboardColor.keyboardAppearance self.codeInputView.textField.returnKeyType = .done self.codeInputView.textField.disableAutomaticKeyboardHandling = [.forward, .backward] + self.codeInputView.textField.accessibilityIdentifier = "Auth.CodeEntry.CodeField" if #available(iOSApplicationExtension 12.0, iOS 12.0, *) { self.codeInputView.textField.textContentType = .oneTimeCode } diff --git a/submodules/AuthorizationUI/Sources/AuthorizationSequenceSignUpControllerNode.swift b/submodules/AuthorizationUI/Sources/AuthorizationSequenceSignUpControllerNode.swift index 56f48e663d..1e8ade8278 100644 --- a/submodules/AuthorizationUI/Sources/AuthorizationSequenceSignUpControllerNode.swift +++ b/submodules/AuthorizationUI/Sources/AuthorizationSequenceSignUpControllerNode.swift @@ -90,7 +90,8 @@ final class AuthorizationSequenceSignUpControllerNode: ASDisplayNode, UITextFiel self.titleNode.isUserInteractionEnabled = false self.titleNode.displaysAsynchronously = false self.titleNode.attributedText = NSAttributedString(string: self.strings.Login_InfoTitle, font: Font.semibold(28.0), textColor: theme.list.itemPrimaryTextColor) - + self.titleNode.accessibilityIdentifier = "Auth.SetName.Title" + self.currentOptionNode = ASTextNode() self.currentOptionNode.isUserInteractionEnabled = false self.currentOptionNode.displaysAsynchronously = false @@ -125,7 +126,8 @@ final class AuthorizationSequenceSignUpControllerNode: ASDisplayNode, UITextFiel } self.firstNameField.textField.keyboardAppearance = theme.rootController.keyboardColor.keyboardAppearance self.firstNameField.textField.tintColor = theme.list.itemAccentColor - + self.firstNameField.textField.accessibilityIdentifier = "Auth.SetName.FirstNameField" + self.lastNameField = TextFieldNode() self.lastNameField.textField.font = Font.regular(20.0) self.lastNameField.textField.textColor = self.theme.list.itemPrimaryTextColor @@ -139,7 +141,8 @@ final class AuthorizationSequenceSignUpControllerNode: ASDisplayNode, UITextFiel } self.lastNameField.textField.keyboardAppearance = theme.rootController.keyboardColor.keyboardAppearance self.lastNameField.textField.tintColor = theme.list.itemAccentColor - + self.lastNameField.textField.accessibilityIdentifier = "Auth.SetName.LastNameField" + self.currentPhotoNode = ASImageNode() self.currentPhotoNode.isUserInteractionEnabled = false self.currentPhotoNode.displaysAsynchronously = false @@ -154,6 +157,7 @@ final class AuthorizationSequenceSignUpControllerNode: ASDisplayNode, UITextFiel self.proceedNode = SolidRoundedButtonNode(title: self.strings.Login_Continue, theme: SolidRoundedButtonTheme(theme: self.theme), glass: false, height: 50.0, cornerRadius: 50.0 * 0.5) self.proceedNode.progressType = .embedded + self.proceedNode.accessibilityIdentifier = "Auth.SetName.ContinueButton" super.init() diff --git a/submodules/CodeInputView/Sources/CodeInputView.swift b/submodules/CodeInputView/Sources/CodeInputView.swift index 74d3b38ee9..68b2801b7d 100644 --- a/submodules/CodeInputView/Sources/CodeInputView.swift +++ b/submodules/CodeInputView/Sources/CodeInputView.swift @@ -153,8 +153,10 @@ public final class CodeInputView: ASDisplayNode, UITextFieldDelegate { super.init() self.addSubnode(self.prefixLabel) + self.textField.frame = CGRect(x: 0, y: 0, width: 1, height: 1) + self.textField.alpha = 0.01 self.view.addSubview(self.textField) - + self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) self.textField.delegate = self self.textField.addTarget(self, action: #selector(self.textFieldChanged(_:)), for: .editingChanged) diff --git a/submodules/TelegramUI/Sources/AppDelegate.swift b/submodules/TelegramUI/Sources/AppDelegate.swift index 6ab1828272..79d469e26d 100644 --- a/submodules/TelegramUI/Sources/AppDelegate.swift +++ b/submodules/TelegramUI/Sources/AppDelegate.swift @@ -656,7 +656,9 @@ private func extractAccountManagerState(records: AccountRecordsView