diff --git a/.gitignore b/.gitignore
index 8678e63e..df1940dc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1 @@
-
-*.xcbkptlist
+*.xcbkptlist
\ No newline at end of file
diff --git a/Gemfile b/Gemfile
new file mode 100644
index 00000000..7a118b49
--- /dev/null
+++ b/Gemfile
@@ -0,0 +1,3 @@
+source "https://rubygems.org"
+
+gem "fastlane"
diff --git a/Gemfile.lock b/Gemfile.lock
new file mode 100644
index 00000000..54e62f84
--- /dev/null
+++ b/Gemfile.lock
@@ -0,0 +1,218 @@
+GEM
+ remote: https://rubygems.org/
+ specs:
+ CFPropertyList (3.0.5)
+ rexml
+ addressable (2.8.1)
+ public_suffix (>= 2.0.2, < 6.0)
+ artifactory (3.0.15)
+ atomos (0.1.3)
+ aws-eventstream (1.2.0)
+ aws-partitions (1.693.0)
+ aws-sdk-core (3.168.4)
+ aws-eventstream (~> 1, >= 1.0.2)
+ aws-partitions (~> 1, >= 1.651.0)
+ aws-sigv4 (~> 1.5)
+ jmespath (~> 1, >= 1.6.1)
+ aws-sdk-kms (1.61.0)
+ aws-sdk-core (~> 3, >= 3.165.0)
+ aws-sigv4 (~> 1.1)
+ aws-sdk-s3 (1.117.2)
+ aws-sdk-core (~> 3, >= 3.165.0)
+ aws-sdk-kms (~> 1)
+ aws-sigv4 (~> 1.4)
+ aws-sigv4 (1.5.2)
+ aws-eventstream (~> 1, >= 1.0.2)
+ babosa (1.0.4)
+ claide (1.1.0)
+ colored (1.2)
+ colored2 (3.1.2)
+ commander (4.6.0)
+ highline (~> 2.0.0)
+ declarative (0.0.20)
+ digest-crc (0.6.4)
+ rake (>= 12.0.0, < 14.0.0)
+ domain_name (0.5.20190701)
+ unf (>= 0.0.5, < 1.0.0)
+ dotenv (2.8.1)
+ emoji_regex (3.2.3)
+ excon (0.97.1)
+ faraday (1.10.2)
+ faraday-em_http (~> 1.0)
+ faraday-em_synchrony (~> 1.0)
+ faraday-excon (~> 1.1)
+ faraday-httpclient (~> 1.0)
+ faraday-multipart (~> 1.0)
+ faraday-net_http (~> 1.0)
+ faraday-net_http_persistent (~> 1.0)
+ faraday-patron (~> 1.0)
+ faraday-rack (~> 1.0)
+ faraday-retry (~> 1.0)
+ ruby2_keywords (>= 0.0.4)
+ faraday-cookie_jar (0.0.7)
+ faraday (>= 0.8.0)
+ http-cookie (~> 1.0.0)
+ faraday-em_http (1.0.0)
+ faraday-em_synchrony (1.0.0)
+ faraday-excon (1.1.0)
+ faraday-httpclient (1.0.1)
+ faraday-multipart (1.0.4)
+ multipart-post (~> 2)
+ faraday-net_http (1.0.1)
+ faraday-net_http_persistent (1.2.0)
+ faraday-patron (1.0.0)
+ faraday-rack (1.0.0)
+ faraday-retry (1.0.3)
+ faraday_middleware (1.2.0)
+ faraday (~> 1.0)
+ fastimage (2.2.6)
+ fastlane (2.211.0)
+ CFPropertyList (>= 2.3, < 4.0.0)
+ addressable (>= 2.8, < 3.0.0)
+ artifactory (~> 3.0)
+ aws-sdk-s3 (~> 1.0)
+ babosa (>= 1.0.3, < 2.0.0)
+ bundler (>= 1.12.0, < 3.0.0)
+ colored
+ commander (~> 4.6)
+ dotenv (>= 2.1.1, < 3.0.0)
+ emoji_regex (>= 0.1, < 4.0)
+ excon (>= 0.71.0, < 1.0.0)
+ faraday (~> 1.0)
+ faraday-cookie_jar (~> 0.0.6)
+ faraday_middleware (~> 1.0)
+ fastimage (>= 2.1.0, < 3.0.0)
+ gh_inspector (>= 1.1.2, < 2.0.0)
+ google-apis-androidpublisher_v3 (~> 0.3)
+ google-apis-playcustomapp_v1 (~> 0.1)
+ google-cloud-storage (~> 1.31)
+ highline (~> 2.0)
+ json (< 3.0.0)
+ jwt (>= 2.1.0, < 3)
+ mini_magick (>= 4.9.4, < 5.0.0)
+ multipart-post (~> 2.0.0)
+ naturally (~> 2.2)
+ optparse (~> 0.1.1)
+ plist (>= 3.1.0, < 4.0.0)
+ rubyzip (>= 2.0.0, < 3.0.0)
+ security (= 0.1.3)
+ simctl (~> 1.6.3)
+ terminal-notifier (>= 2.0.0, < 3.0.0)
+ terminal-table (>= 1.4.5, < 2.0.0)
+ tty-screen (>= 0.6.3, < 1.0.0)
+ tty-spinner (>= 0.8.0, < 1.0.0)
+ word_wrap (~> 1.0.0)
+ xcodeproj (>= 1.13.0, < 2.0.0)
+ xcpretty (~> 0.3.0)
+ xcpretty-travis-formatter (>= 0.0.3)
+ gh_inspector (1.1.3)
+ google-apis-androidpublisher_v3 (0.32.0)
+ google-apis-core (>= 0.9.1, < 2.a)
+ google-apis-core (0.9.5)
+ addressable (~> 2.5, >= 2.5.1)
+ googleauth (>= 0.16.2, < 2.a)
+ httpclient (>= 2.8.1, < 3.a)
+ mini_mime (~> 1.0)
+ representable (~> 3.0)
+ retriable (>= 2.0, < 4.a)
+ rexml
+ webrick
+ google-apis-iamcredentials_v1 (0.16.0)
+ google-apis-core (>= 0.9.1, < 2.a)
+ google-apis-playcustomapp_v1 (0.12.0)
+ google-apis-core (>= 0.9.1, < 2.a)
+ google-apis-storage_v1 (0.19.0)
+ google-apis-core (>= 0.9.0, < 2.a)
+ google-cloud-core (1.6.0)
+ google-cloud-env (~> 1.0)
+ google-cloud-errors (~> 1.0)
+ google-cloud-env (1.6.0)
+ faraday (>= 0.17.3, < 3.0)
+ google-cloud-errors (1.3.0)
+ google-cloud-storage (1.44.0)
+ addressable (~> 2.8)
+ digest-crc (~> 0.4)
+ google-apis-iamcredentials_v1 (~> 0.1)
+ google-apis-storage_v1 (~> 0.19.0)
+ google-cloud-core (~> 1.6)
+ googleauth (>= 0.16.2, < 2.a)
+ mini_mime (~> 1.0)
+ googleauth (1.3.0)
+ faraday (>= 0.17.3, < 3.a)
+ jwt (>= 1.4, < 3.0)
+ memoist (~> 0.16)
+ multi_json (~> 1.11)
+ os (>= 0.9, < 2.0)
+ signet (>= 0.16, < 2.a)
+ highline (2.0.3)
+ http-cookie (1.0.5)
+ domain_name (~> 0.5)
+ httpclient (2.8.3)
+ jmespath (1.6.2)
+ json (2.6.3)
+ jwt (2.6.0)
+ memoist (0.16.2)
+ mini_magick (4.12.0)
+ mini_mime (1.1.2)
+ multi_json (1.15.0)
+ multipart-post (2.0.0)
+ nanaimo (0.3.0)
+ naturally (2.2.1)
+ optparse (0.1.1)
+ os (1.1.4)
+ plist (3.6.0)
+ public_suffix (5.0.1)
+ rake (13.0.6)
+ representable (3.2.0)
+ declarative (< 0.1.0)
+ trailblazer-option (>= 0.1.1, < 0.2.0)
+ uber (< 0.2.0)
+ retriable (3.1.2)
+ rexml (3.2.5)
+ rouge (2.0.7)
+ ruby2_keywords (0.0.5)
+ rubyzip (2.3.2)
+ security (0.1.3)
+ signet (0.17.0)
+ addressable (~> 2.8)
+ faraday (>= 0.17.5, < 3.a)
+ jwt (>= 1.5, < 3.0)
+ multi_json (~> 1.10)
+ simctl (1.6.8)
+ CFPropertyList
+ naturally
+ terminal-notifier (2.0.0)
+ terminal-table (1.8.0)
+ unicode-display_width (~> 1.1, >= 1.1.1)
+ trailblazer-option (0.1.2)
+ tty-cursor (0.7.1)
+ tty-screen (0.8.1)
+ tty-spinner (0.9.3)
+ tty-cursor (~> 0.7)
+ uber (0.1.0)
+ unf (0.1.4)
+ unf_ext
+ unf_ext (0.0.8.2)
+ unicode-display_width (1.8.0)
+ webrick (1.7.0)
+ word_wrap (1.0.0)
+ xcodeproj (1.22.0)
+ CFPropertyList (>= 2.3.3, < 4.0)
+ atomos (~> 0.1.3)
+ claide (>= 1.0.2, < 2.0)
+ colored2 (~> 3.1)
+ nanaimo (~> 0.3.0)
+ rexml (~> 3.2.4)
+ xcpretty (0.3.0)
+ rouge (~> 2.0.7)
+ xcpretty-travis-formatter (1.0.1)
+ xcpretty (~> 0.2, >= 0.0.7)
+
+PLATFORMS
+ x86_64-darwin-22
+
+DEPENDENCIES
+ fastlane
+
+BUNDLED WITH
+ 2.4.3
diff --git a/README.md b/README.md
index fb6d2492..7e1b1254 100644
--- a/README.md
+++ b/README.md
@@ -2,46 +2,53 @@
[Ссылка на приложение в AppStore](https://itunes.apple.com/us/app/jobsy/id1035159361)
## Помощь проекту
-1. Для доработок создаем issue с подробным описанием
+1. Для доработок создаем **issue** с описанием задачи
2. Доработки делаем в отдельных ветках
-3. Для каждого PR необходимо оставить описание на русском языке по аналогии со старыми PR
-4. Вопросы по iOS-приложению решаем с o.n.eremenko@gmail.com (telegram: @Oleg991)
-5. Вопросы по бэкенду/сайту - с anton@workout.su
-
-## Гайд для менеджера
+3. Для каждого **PR** необходимо оставить описание на русском языке по аналогии со старыми **PR**
+4. Вопросы по iOS-приложению решаем с Oleg991 в [почте](mailto:o.n.eremenko@gmail.com?subject=[GitHub]-SwiftUI-WorkoutApp) или в [телеграм](http://t.me/oleg991)
+5. Вопросы по бэкенду/сайту - c Антоном в [почте](mailto:anton@workout.su?subject=[GitHub]-SwiftUI-WorkoutApp)
+## Шпаргалка
### Настройка базовых параметров приложения
-Xcode -> SwiftUI-WorkoutApp -> Target: SwiftUI-WorkoutApp -> General
-- Display Name - название приложения на экране смартфона
-- Version - версия приложения для магазина
-- Build - версия сборки для TestFlight
+`Xcode -> SwiftUI-WorkoutApp -> Target: SwiftUI-WorkoutApp -> General`
+- `Display Name` - название приложения на экране смартфона
+- `Version` - версия приложения для магазина
+- `Build` - версия сборки для `TestFlight`
### Публикация приложения
#### TestFlight
1. Скачать актуальную версию репозитория
-2. Открыть Xcode и дождаться загрузки зависимостей; при возникновении ошибок можно:
- - почистить Derived Data и память билда (command + shift + k)
- - обновить зависимости (File -> Packages -> Reset/Resolve/Update)
-3. В верхней панели Xcode сменить девайс на Any iOS Device
-4. Product -> Archive
-5. Дождаться архивации, в открывшемся окне со сборками выбрать нужную и нажать Distribute App
+2. Открыть `Xcode` и дождаться загрузки зависимостей; при возникновении ошибок можно:
+ - почистить `Derived Data` и память билда (`command + shift + k`)
+ - обновить зависимости (`File -> Packages -> Reset/Resolve/Update`)
+3. В верхней панели Xcode сменить девайс на `Any iOS Device `
+4. `Product` -> `Archive`
+5. Дождаться архивации, в открывшемся окне со сборками выбрать нужную и нажать **Distribute App**
6. Пройти по всем шагам и снять галку с автоматического изменения версии сборки на одном из финальных шагов
#### AppStore
-1. Открыть страницу с приложением в AppstoreConnect
-2. В левом меню рядом с версией в статусе "Готово к продаже" нажать плюс и добавить новую версию
-3. Заполнить поле "Что нового в этой версии"
-4. Ниже в разделе "Сборка" выбрать нужную сборку TestFlight
+1. Открыть страницу с приложением в **AppstoreConnect**
+2. В левом меню рядом с версией в статусе **Готово к продаже** нажать `+` и добавить новую версию
+3. Заполнить поле **Что нового в этой версии**
+4. Ниже в разделе **Сборка** выбрать нужную сборку из `TestFlight`
5. Ниже на странице проверить галки
- - "Выпустить эту версию автоматически"
- - "Выпустить обновление сразу для всех пользователей"
- - "Сохранить текущую оценку"
-6. Нажать сверху справа кнопку "Сохранить"
+ - *Выпустить эту версию автоматически*
+ - *Выпустить обновление сразу для всех пользователей*
+ - *Сохранить текущую оценку*
+6. Нажать сверху справа кнопку **Сохранить**
7. Отправить приложение на проверку
### Скриншоты
-Необходимо сменить язык симулятора на русский перед созданием скриншотов
-- 6.5 дюйма: iPhone 13 pro max
-- 5.8 дюйма: iPhone 13 pro
-- 5.5 дюйма: iPhone 8 plus
-- 4.7 дюйма: iPhone 8
+1. Генерируем скриншоты при помощи `Fastlane` ([документация](https://docs.fastlane.tools/getting-started/ios/setup/))
+2. Настройки для генерации скриншотов находятся в файле [Snapfile](Snapfile) ([документация](https://docs.fastlane.tools/actions/snapshot/))
+3. Готовые скриншоты сохраняются в папке [screenshots/ru](./screenshots/ru)
+
+| Профиль | Площадка | Прошедшие мероприятия | Мероприятие |
+| --- | --- | --- | --- |
+|
|
|
|
|
+
+#### Модели девайсов, используемые для скриншотов
+- 6.5 дюйма: iPhone 13 Pro Max
+- 5.8 дюйма: iPhone 13 Pro
+- 5.5 дюйма: iPhone 8 Plus
+- 4.7 дюйма: iPhone SE (3rd generation)
diff --git a/Snapfile b/Snapfile
new file mode 100644
index 00000000..ef696687
--- /dev/null
+++ b/Snapfile
@@ -0,0 +1,43 @@
+# A list of devices you want to take the screenshots from
+devices([
+ "iPhone SE (3rd generation)",
+ "iPhone 8 Plus",
+ "iPhone 13 Pro",
+ "iPhone 13 Pro Max",
+])
+
+# A list of languages which should be used. See https://docs.fastlane.tools/actions/snapshot/#available-language-codes
+languages(["ru"])
+
+# By default, the latest version should be used automatically. If you want to change it, do it here.
+# ios_version("15")
+
+# Enabling this option will configure the Simulator to be in dark mode (false for light, true for dark)
+dark_mode(true)
+
+# Enabling this option will configure the Simulator's system language
+localize_simulator(true)
+
+# Should snapshot stop immediately after the tests completely failed on one device?
+stop_after_first_error(true)
+
+# Prevents packages from automatically being resolved to versions other than those recorded in the Package.resolved file
+disable_package_automatic_updates(true)
+
+# The name of the scheme which contains the UI Tests
+scheme("WorkoutAppUITests")
+
+# Where should the resulting screenshots be stored?
+output_directory("./screenshots")
+
+# remove the '#' to clear all previously generated screenshots before creating new ones
+clear_previous_screenshots(true)
+
+# Enabling this option will automatically override the status bar to show 9:41 AM, full battery, and full reception
+override_status_bar(true)
+
+# Arguments to pass to the app on launch. See https://docs.fastlane.tools/actions/snapshot/#launch-arguments
+launch_arguments(["UITest"])
+
+# For more information about all available options run
+# fastlane action snapshot
diff --git a/SnapshotHelper.swift b/SnapshotHelper.swift
new file mode 100644
index 00000000..da063ba1
--- /dev/null
+++ b/SnapshotHelper.swift
@@ -0,0 +1,309 @@
+//
+// SnapshotHelper.swift
+// Example
+//
+// Created by Felix Krause on 10/8/15.
+//
+
+// -----------------------------------------------------
+// IMPORTANT: When modifying this file, make sure to
+// increment the version number at the very
+// bottom of the file to notify users about
+// the new SnapshotHelper.swift
+// -----------------------------------------------------
+
+import Foundation
+import XCTest
+
+var deviceLanguage = ""
+var locale = ""
+
+func setupSnapshot(_ app: XCUIApplication, waitForAnimations: Bool = true) {
+ Snapshot.setupSnapshot(app, waitForAnimations: waitForAnimations)
+}
+
+func snapshot(_ name: String, waitForLoadingIndicator: Bool) {
+ if waitForLoadingIndicator {
+ Snapshot.snapshot(name)
+ } else {
+ Snapshot.snapshot(name, timeWaitingForIdle: 0)
+ }
+}
+
+/// - Parameters:
+/// - name: The name of the snapshot
+/// - timeout: Amount of seconds to wait until the network loading indicator disappears. Pass `0` if you don't want to wait.
+func snapshot(_ name: String, timeWaitingForIdle timeout: TimeInterval = 20) {
+ Snapshot.snapshot(name, timeWaitingForIdle: timeout)
+}
+
+enum SnapshotError: Error, CustomDebugStringConvertible {
+ case cannotFindSimulatorHomeDirectory
+ case cannotRunOnPhysicalDevice
+
+ var debugDescription: String {
+ switch self {
+ case .cannotFindSimulatorHomeDirectory:
+ return "Couldn't find simulator home location. Please, check SIMULATOR_HOST_HOME env variable."
+ case .cannotRunOnPhysicalDevice:
+ return "Can't use Snapshot on a physical device."
+ }
+ }
+}
+
+@objcMembers
+open class Snapshot: NSObject {
+ static var app: XCUIApplication?
+ static var waitForAnimations = true
+ static var cacheDirectory: URL?
+ static var screenshotsDirectory: URL? {
+ return cacheDirectory?.appendingPathComponent("screenshots", isDirectory: true)
+ }
+
+ open class func setupSnapshot(_ app: XCUIApplication, waitForAnimations: Bool = true) {
+
+ Snapshot.app = app
+ Snapshot.waitForAnimations = waitForAnimations
+
+ do {
+ let cacheDir = try getCacheDirectory()
+ Snapshot.cacheDirectory = cacheDir
+ setLanguage(app)
+ setLocale(app)
+ setLaunchArguments(app)
+ } catch let error {
+ NSLog(error.localizedDescription)
+ }
+ }
+
+ class func setLanguage(_ app: XCUIApplication) {
+ guard let cacheDirectory = self.cacheDirectory else {
+ NSLog("CacheDirectory is not set - probably running on a physical device?")
+ return
+ }
+
+ let path = cacheDirectory.appendingPathComponent("language.txt")
+
+ do {
+ let trimCharacterSet = CharacterSet.whitespacesAndNewlines
+ deviceLanguage = try String(contentsOf: path, encoding: .utf8).trimmingCharacters(in: trimCharacterSet)
+ app.launchArguments += ["-AppleLanguages", "(\(deviceLanguage))"]
+ } catch {
+ NSLog("Couldn't detect/set language...")
+ }
+ }
+
+ class func setLocale(_ app: XCUIApplication) {
+ guard let cacheDirectory = self.cacheDirectory else {
+ NSLog("CacheDirectory is not set - probably running on a physical device?")
+ return
+ }
+
+ let path = cacheDirectory.appendingPathComponent("locale.txt")
+
+ do {
+ let trimCharacterSet = CharacterSet.whitespacesAndNewlines
+ locale = try String(contentsOf: path, encoding: .utf8).trimmingCharacters(in: trimCharacterSet)
+ } catch {
+ NSLog("Couldn't detect/set locale...")
+ }
+
+ if locale.isEmpty && !deviceLanguage.isEmpty {
+ locale = Locale(identifier: deviceLanguage).identifier
+ }
+
+ if !locale.isEmpty {
+ app.launchArguments += ["-AppleLocale", "\"\(locale)\""]
+ }
+ }
+
+ class func setLaunchArguments(_ app: XCUIApplication) {
+ guard let cacheDirectory = self.cacheDirectory else {
+ NSLog("CacheDirectory is not set - probably running on a physical device?")
+ return
+ }
+
+ let path = cacheDirectory.appendingPathComponent("snapshot-launch_arguments.txt")
+ app.launchArguments += ["-FASTLANE_SNAPSHOT", "YES", "-ui_testing"]
+
+ do {
+ let launchArguments = try String(contentsOf: path, encoding: String.Encoding.utf8)
+ let regex = try NSRegularExpression(pattern: "(\\\".+?\\\"|\\S+)", options: [])
+ let matches = regex.matches(in: launchArguments, options: [], range: NSRange(location: 0, length: launchArguments.count))
+ let results = matches.map { result -> String in
+ (launchArguments as NSString).substring(with: result.range)
+ }
+ app.launchArguments += results
+ } catch {
+ NSLog("Couldn't detect/set launch_arguments...")
+ }
+ }
+
+ open class func snapshot(_ name: String, timeWaitingForIdle timeout: TimeInterval = 20) {
+ if timeout > 0 {
+ waitForLoadingIndicatorToDisappear(within: timeout)
+ }
+
+ NSLog("snapshot: \(name)") // more information about this, check out https://docs.fastlane.tools/actions/snapshot/#how-does-it-work
+
+ if Snapshot.waitForAnimations {
+ sleep(1) // Waiting for the animation to be finished (kind of)
+ }
+
+ #if os(OSX)
+ guard let app = self.app else {
+ NSLog("XCUIApplication is not set. Please call setupSnapshot(app) before snapshot().")
+ return
+ }
+
+ app.typeKey(XCUIKeyboardKeySecondaryFn, modifierFlags: [])
+ #else
+
+ guard self.app != nil else {
+ NSLog("XCUIApplication is not set. Please call setupSnapshot(app) before snapshot().")
+ return
+ }
+
+ let screenshot = XCUIScreen.main.screenshot()
+ #if os(iOS) && !targetEnvironment(macCatalyst)
+ let image = XCUIDevice.shared.orientation.isLandscape ? fixLandscapeOrientation(image: screenshot.image) : screenshot.image
+ #else
+ let image = screenshot.image
+ #endif
+
+ guard var simulator = ProcessInfo().environment["SIMULATOR_DEVICE_NAME"], let screenshotsDir = screenshotsDirectory else { return }
+
+ do {
+ // The simulator name contains "Clone X of " inside the screenshot file when running parallelized UI Tests on concurrent devices
+ let regex = try NSRegularExpression(pattern: "Clone [0-9]+ of ")
+ let range = NSRange(location: 0, length: simulator.count)
+ simulator = regex.stringByReplacingMatches(in: simulator, range: range, withTemplate: "")
+
+ let path = screenshotsDir.appendingPathComponent("\(simulator)-\(name).png")
+ #if swift(<5.0)
+ try UIImagePNGRepresentation(image)?.write(to: path, options: .atomic)
+ #else
+ try image.pngData()?.write(to: path, options: .atomic)
+ #endif
+ } catch let error {
+ NSLog("Problem writing screenshot: \(name) to \(screenshotsDir)/\(simulator)-\(name).png")
+ NSLog(error.localizedDescription)
+ }
+ #endif
+ }
+
+ class func fixLandscapeOrientation(image: UIImage) -> UIImage {
+ #if os(watchOS)
+ return image
+ #else
+ if #available(iOS 10.0, *) {
+ let format = UIGraphicsImageRendererFormat()
+ format.scale = image.scale
+ let renderer = UIGraphicsImageRenderer(size: image.size, format: format)
+ return renderer.image { context in
+ image.draw(in: CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height))
+ }
+ } else {
+ return image
+ }
+ #endif
+ }
+
+ class func waitForLoadingIndicatorToDisappear(within timeout: TimeInterval) {
+ #if os(tvOS)
+ return
+ #endif
+
+ guard let app = self.app else {
+ NSLog("XCUIApplication is not set. Please call setupSnapshot(app) before snapshot().")
+ return
+ }
+
+ let networkLoadingIndicator = app.otherElements.deviceStatusBars.networkLoadingIndicators.element
+ let networkLoadingIndicatorDisappeared = XCTNSPredicateExpectation(predicate: NSPredicate(format: "exists == false"), object: networkLoadingIndicator)
+ _ = XCTWaiter.wait(for: [networkLoadingIndicatorDisappeared], timeout: timeout)
+ }
+
+ class func getCacheDirectory() throws -> URL {
+ let cachePath = "Library/Caches/tools.fastlane"
+ // on OSX config is stored in /Users//Library
+ // and on iOS/tvOS/WatchOS it's in simulator's home dir
+ #if os(OSX)
+ let homeDir = URL(fileURLWithPath: NSHomeDirectory())
+ return homeDir.appendingPathComponent(cachePath)
+ #elseif arch(i386) || arch(x86_64) || arch(arm64)
+ guard let simulatorHostHome = ProcessInfo().environment["SIMULATOR_HOST_HOME"] else {
+ throw SnapshotError.cannotFindSimulatorHomeDirectory
+ }
+ let homeDir = URL(fileURLWithPath: simulatorHostHome)
+ return homeDir.appendingPathComponent(cachePath)
+ #else
+ throw SnapshotError.cannotRunOnPhysicalDevice
+ #endif
+ }
+}
+
+private extension XCUIElementAttributes {
+ var isNetworkLoadingIndicator: Bool {
+ if hasAllowListedIdentifier { return false }
+
+ let hasOldLoadingIndicatorSize = frame.size == CGSize(width: 10, height: 20)
+ let hasNewLoadingIndicatorSize = frame.size.width.isBetween(46, and: 47) && frame.size.height.isBetween(2, and: 3)
+
+ return hasOldLoadingIndicatorSize || hasNewLoadingIndicatorSize
+ }
+
+ var hasAllowListedIdentifier: Bool {
+ let allowListedIdentifiers = ["GeofenceLocationTrackingOn", "StandardLocationTrackingOn"]
+
+ return allowListedIdentifiers.contains(identifier)
+ }
+
+ func isStatusBar(_ deviceWidth: CGFloat) -> Bool {
+ if elementType == .statusBar { return true }
+ guard frame.origin == .zero else { return false }
+
+ let oldStatusBarSize = CGSize(width: deviceWidth, height: 20)
+ let newStatusBarSize = CGSize(width: deviceWidth, height: 44)
+
+ return [oldStatusBarSize, newStatusBarSize].contains(frame.size)
+ }
+}
+
+private extension XCUIElementQuery {
+ var networkLoadingIndicators: XCUIElementQuery {
+ let isNetworkLoadingIndicator = NSPredicate { (evaluatedObject, _) in
+ guard let element = evaluatedObject as? XCUIElementAttributes else { return false }
+
+ return element.isNetworkLoadingIndicator
+ }
+
+ return self.containing(isNetworkLoadingIndicator)
+ }
+
+ var deviceStatusBars: XCUIElementQuery {
+ guard let app = Snapshot.app else {
+ fatalError("XCUIApplication is not set. Please call setupSnapshot(app) before snapshot().")
+ }
+
+ let deviceWidth = app.windows.firstMatch.frame.width
+
+ let isStatusBar = NSPredicate { (evaluatedObject, _) in
+ guard let element = evaluatedObject as? XCUIElementAttributes else { return false }
+
+ return element.isStatusBar(deviceWidth)
+ }
+
+ return self.containing(isStatusBar)
+ }
+}
+
+private extension CGFloat {
+ func isBetween(_ numberA: CGFloat, and numberB: CGFloat) -> Bool {
+ return numberA...numberB ~= self
+ }
+}
+
+// Please don't remove the lines below
+// They are used to detect outdated configuration files
+// SnapshotHelperVersion [1.29]
diff --git a/SwiftUI-WorkoutApp.xcodeproj/project.pbxproj b/SwiftUI-WorkoutApp.xcodeproj/project.pbxproj
index a62cc32c..60d82d03 100644
--- a/SwiftUI-WorkoutApp.xcodeproj/project.pbxproj
+++ b/SwiftUI-WorkoutApp.xcodeproj/project.pbxproj
@@ -17,6 +17,10 @@
670F16C82826D1090069D80A /* UsersListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 670F16C72826D1090069D80A /* UsersListViewModel.swift */; };
67138D792971CA7A00BBF450 /* MessagingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67138D782971CA7A00BBF450 /* MessagingViewModel.swift */; };
67138D7D2972E86C00BBF450 /* IncognitoNavbarInfoButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67138D7C2972E86C00BBF450 /* IncognitoNavbarInfoButton.swift */; };
+ 67138D87297311BD00BBF450 /* WorkoutAppUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67138D86297311BD00BBF450 /* WorkoutAppUITests.swift */; };
+ 67138D90297323AE00BBF450 /* SnapshotHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67138D8F297323AE00BBF450 /* SnapshotHelper.swift */; };
+ 67138D922974851F00BBF450 /* XCUIElement+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67138D912974851F00BBF450 /* XCUIElement+.swift */; };
+ 67138D942974854F00BBF450 /* XCTestCase+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67138D932974854F00BBF450 /* XCTestCase+.swift */; };
671D7DEC28210D2F0068E728 /* EmptyContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 671D7DEB28210D2F0068E728 /* EmptyContentView.swift */; };
671D7DEF282112140068E728 /* Text+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 671D7DEE282112140068E728 /* Text+.swift */; };
671D7DF1282117FF0068E728 /* UserViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 671D7DF0282117FF0068E728 /* UserViewCell.swift */; };
@@ -153,6 +157,16 @@
67FBF64F28338A2E008A7968 /* EventDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67FBF64E28338A2E008A7968 /* EventDetailsView.swift */; };
/* End PBXBuildFile section */
+/* Begin PBXContainerItemProxy section */
+ 67138D8A297311BD00BBF450 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 6798AA32280AEDC900DB76F1 /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = 6798AA39280AEDC900DB76F1;
+ remoteInfo = "SwiftUI-WorkoutApp";
+ };
+/* End PBXContainerItemProxy section */
+
/* Begin PBXFileReference section */
6705E7EB283B5DCF00DABCC8 /* View+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+.swift"; sourceTree = ""; };
6705E7ED283B703400DABCC8 /* JournalSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JournalSettingsView.swift; sourceTree = ""; };
@@ -163,6 +177,11 @@
670F16C72826D1090069D80A /* UsersListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UsersListViewModel.swift; sourceTree = ""; };
67138D782971CA7A00BBF450 /* MessagingViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessagingViewModel.swift; sourceTree = ""; };
67138D7C2972E86C00BBF450 /* IncognitoNavbarInfoButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncognitoNavbarInfoButton.swift; sourceTree = ""; };
+ 67138D84297311BD00BBF450 /* WorkoutAppUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = WorkoutAppUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
+ 67138D86297311BD00BBF450 /* WorkoutAppUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkoutAppUITests.swift; sourceTree = ""; };
+ 67138D8F297323AE00BBF450 /* SnapshotHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SnapshotHelper.swift; sourceTree = SOURCE_ROOT; };
+ 67138D912974851F00BBF450 /* XCUIElement+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XCUIElement+.swift"; sourceTree = ""; };
+ 67138D932974854F00BBF450 /* XCTestCase+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XCTestCase+.swift"; sourceTree = ""; };
671D7DEB28210D2F0068E728 /* EmptyContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyContentView.swift; sourceTree = ""; };
671D7DEE282112140068E728 /* Text+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Text+.swift"; sourceTree = ""; };
671D7DF0282117FF0068E728 /* UserViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserViewCell.swift; sourceTree = ""; };
@@ -302,6 +321,13 @@
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
+ 67138D81297311BD00BBF450 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
6798AA37280AEDC900DB76F1 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
@@ -342,6 +368,17 @@
path = ChangePassword;
sourceTree = "";
};
+ 67138D85297311BD00BBF450 /* WorkoutAppUITests */ = {
+ isa = PBXGroup;
+ children = (
+ 67138D8F297323AE00BBF450 /* SnapshotHelper.swift */,
+ 67138D86297311BD00BBF450 /* WorkoutAppUITests.swift */,
+ 67138D912974851F00BBF450 /* XCUIElement+.swift */,
+ 67138D932974854F00BBF450 /* XCTestCase+.swift */,
+ );
+ path = WorkoutAppUITests;
+ sourceTree = "";
+ };
671D7DED28210D820068E728 /* Events */ = {
isa = PBXGroup;
children = (
@@ -564,6 +601,7 @@
children = (
675A3707285480E600DAE071 /* Utils */,
6798AA3C280AEDC900DB76F1 /* SwiftUI-WorkoutApp */,
+ 67138D85297311BD00BBF450 /* WorkoutAppUITests */,
6798AA3B280AEDC900DB76F1 /* Products */,
675A37082854810B00DAE071 /* Frameworks */,
);
@@ -573,6 +611,7 @@
isa = PBXGroup;
children = (
6798AA3A280AEDC900DB76F1 /* WorkoutApp.app */,
+ 67138D84297311BD00BBF450 /* WorkoutAppUITests.xctest */,
);
name = Products;
sourceTree = "";
@@ -844,6 +883,24 @@
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
+ 67138D83297311BD00BBF450 /* WorkoutAppUITests */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 67138D8E297311BD00BBF450 /* Build configuration list for PBXNativeTarget "WorkoutAppUITests" */;
+ buildPhases = (
+ 67138D80297311BD00BBF450 /* Sources */,
+ 67138D81297311BD00BBF450 /* Frameworks */,
+ 67138D82297311BD00BBF450 /* Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ 67138D8B297311BD00BBF450 /* PBXTargetDependency */,
+ );
+ name = WorkoutAppUITests;
+ productName = WorkoutAppUITests;
+ productReference = 67138D84297311BD00BBF450 /* WorkoutAppUITests.xctest */;
+ productType = "com.apple.product-type.bundle.ui-testing";
+ };
6798AA39280AEDC900DB76F1 /* SwiftUI-WorkoutApp */ = {
isa = PBXNativeTarget;
buildConfigurationList = 6798AA48280AEDCA00DB76F1 /* Build configuration list for PBXNativeTarget "SwiftUI-WorkoutApp" */;
@@ -873,9 +930,13 @@
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = 1;
- LastSwiftUpdateCheck = 1330;
+ LastSwiftUpdateCheck = 1420;
LastUpgradeCheck = 1330;
TargetAttributes = {
+ 67138D83297311BD00BBF450 = {
+ CreatedOnToolsVersion = 14.2;
+ TestTargetID = 6798AA39280AEDC900DB76F1;
+ };
6798AA39280AEDC900DB76F1 = {
CreatedOnToolsVersion = 13.3.1;
};
@@ -900,11 +961,19 @@
projectRoot = "";
targets = (
6798AA39280AEDC900DB76F1 /* SwiftUI-WorkoutApp */,
+ 67138D83297311BD00BBF450 /* WorkoutAppUITests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
+ 67138D82297311BD00BBF450 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
6798AA38280AEDC900DB76F1 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
@@ -923,6 +992,17 @@
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
+ 67138D80297311BD00BBF450 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 67138D87297311BD00BBF450 /* WorkoutAppUITests.swift in Sources */,
+ 67138D922974851F00BBF450 /* XCUIElement+.swift in Sources */,
+ 67138D942974854F00BBF450 /* XCTestCase+.swift in Sources */,
+ 67138D90297323AE00BBF450 /* SnapshotHelper.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
6798AA36280AEDC900DB76F1 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
@@ -1065,6 +1145,14 @@
};
/* End PBXSourcesBuildPhase section */
+/* Begin PBXTargetDependency section */
+ 67138D8B297311BD00BBF450 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = 6798AA39280AEDC900DB76F1 /* SwiftUI-WorkoutApp */;
+ targetProxy = 67138D8A297311BD00BBF450 /* PBXContainerItemProxy */;
+ };
+/* End PBXTargetDependency section */
+
/* Begin PBXVariantGroup section */
675A370528547ACA00DAE071 /* Localizable.stringsdict */ = {
isa = PBXVariantGroup;
@@ -1086,6 +1174,51 @@
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
+ 67138D8C297311BD00BBF450 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_TEAM = 3PHS45582J;
+ GENERATE_INFOPLIST_FILE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 15;
+ MARKETING_VERSION = 1.0;
+ MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
+ PRODUCT_BUNDLE_IDENTIFIER = com.oleg991.WorkoutAppUITests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
+ SUPPORTS_MACCATALYST = NO;
+ SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
+ SWIFT_EMIT_LOC_STRINGS = NO;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = 1;
+ TEST_TARGET_NAME = "SwiftUI-WorkoutApp";
+ };
+ name = Debug;
+ };
+ 67138D8D297311BD00BBF450 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_TEAM = 3PHS45582J;
+ GENERATE_INFOPLIST_FILE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 15;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = com.oleg991.WorkoutAppUITests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
+ SUPPORTS_MACCATALYST = NO;
+ SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
+ SWIFT_EMIT_LOC_STRINGS = NO;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = 1;
+ TEST_TARGET_NAME = "SwiftUI-WorkoutApp";
+ };
+ name = Release;
+ };
6798AA46280AEDCA00DB76F1 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
@@ -1210,7 +1343,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 6;
+ CURRENT_PROJECT_VERSION = 7;
DEVELOPMENT_ASSET_PATHS = "SwiftUI-WorkoutApp/Preview\\ Content/PreviewContent.swift SwiftUI-WorkoutApp/Preview\\ Content";
DEVELOPMENT_TEAM = CR68PP2Z3F;
ENABLE_PREVIEWS = YES;
@@ -1246,7 +1379,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 6;
+ CURRENT_PROJECT_VERSION = 7;
DEVELOPMENT_ASSET_PATHS = "SwiftUI-WorkoutApp/Preview\\ Content/PreviewContent.swift SwiftUI-WorkoutApp/Preview\\ Content";
DEVELOPMENT_TEAM = CR68PP2Z3F;
ENABLE_PREVIEWS = YES;
@@ -1279,6 +1412,15 @@
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
+ 67138D8E297311BD00BBF450 /* Build configuration list for PBXNativeTarget "WorkoutAppUITests" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 67138D8C297311BD00BBF450 /* Debug */,
+ 67138D8D297311BD00BBF450 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
6798AA35280AEDC900DB76F1 /* Build configuration list for PBXProject "SwiftUI-WorkoutApp" */ = {
isa = XCConfigurationList;
buildConfigurations = (
diff --git a/SwiftUI-WorkoutApp.xcodeproj/xcshareddata/xcschemes/SwiftUI-WorkoutApp.xcscheme b/SwiftUI-WorkoutApp.xcodeproj/xcshareddata/xcschemes/SwiftUI-WorkoutApp.xcscheme
index dc6bb93c..cb1e3e93 100644
--- a/SwiftUI-WorkoutApp.xcodeproj/xcshareddata/xcschemes/SwiftUI-WorkoutApp.xcscheme
+++ b/SwiftUI-WorkoutApp.xcodeproj/xcshareddata/xcschemes/SwiftUI-WorkoutApp.xcscheme
@@ -1,7 +1,7 @@
+ version = "1.7">
@@ -26,8 +26,25 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
- shouldUseLaunchSchemeArgsEnv = "YES">
+ shouldUseLaunchSchemeArgsEnv = "YES"
+ language = "ru"
+ region = "RU">
+
+
+
+
+
+
+ allowLocationSimulation = "NO">
+ isEnabled = "NO">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/SwiftUI-WorkoutApp.xcodeproj/xcuserdata/oleg991.xcuserdatad/xcschemes/xcschememanagement.plist b/SwiftUI-WorkoutApp.xcodeproj/xcuserdata/oleg991.xcuserdatad/xcschemes/xcschememanagement.plist
index 87e63aca..eefe8b2d 100644
--- a/SwiftUI-WorkoutApp.xcodeproj/xcuserdata/oleg991.xcuserdatad/xcschemes/xcschememanagement.plist
+++ b/SwiftUI-WorkoutApp.xcodeproj/xcuserdata/oleg991.xcuserdatad/xcschemes/xcschememanagement.plist
@@ -9,9 +9,19 @@
orderHint
0
+ WorkoutAppUITests.xcscheme_^#shared#^_
+
+ orderHint
+ 1
+
SuppressBuildableAutocreation
+ 67138D83297311BD00BBF450
+
+ primary
+
+
6798AA39280AEDC900DB76F1
primary
diff --git a/SwiftUI-WorkoutApp/Screens/Events/List/EventsListView.swift b/SwiftUI-WorkoutApp/Screens/Events/List/EventsListView.swift
index e79753eb..8a660bca 100644
--- a/SwiftUI-WorkoutApp/Screens/Events/List/EventsListView.swift
+++ b/SwiftUI-WorkoutApp/Screens/Events/List/EventsListView.swift
@@ -71,7 +71,10 @@ private extension EventsListView {
var segmentedControl: some View {
Picker("Тип мероприятия", selection: $selectedEventType) {
- ForEach(EventType.allCases, id: \.self) { Text($0.rawValue) }
+ ForEach(EventType.allCases, id: \.self) {
+ Text($0.rawValue)
+ .accessibilityIdentifier($0.rawValue)
+ }
}
.pickerStyle(.segmented)
.padding(.horizontal)
@@ -88,6 +91,7 @@ private extension EventsListView {
NavigationLink(destination: EventDetailsView(with: event, deleteClbk: refreshAction)) {
EventViewCell(for: $event)
}
+ .accessibilityIdentifier("EventViewCell")
}
.opacity(viewModel.isLoading ? 0 : 1)
}
diff --git a/SwiftUI-WorkoutApp/Screens/Profile/Detail/UserDetailsView.swift b/SwiftUI-WorkoutApp/Screens/Profile/Detail/UserDetailsView.swift
index c81499bc..de8a379b 100644
--- a/SwiftUI-WorkoutApp/Screens/Profile/Detail/UserDetailsView.swift
+++ b/SwiftUI-WorkoutApp/Screens/Profile/Detail/UserDetailsView.swift
@@ -177,6 +177,7 @@ private extension UserDetailsView {
Label("Где тренируется", systemImage: "mappin.and.ellipse")
.badge(viewModel.user.usesSportsGrounds.description)
}
+ .accessibilityIdentifier("usesSportsGroundsButton")
}
var addedSportsGroundsButton: some View {
@@ -226,6 +227,7 @@ private extension UserDetailsView {
Image(systemName: "magnifyingglass")
}
.disabled(!network.isConnected)
+ .accessibilityIdentifier("searchUsersButton")
}
var settingsButton: some View {
diff --git a/SwiftUI-WorkoutApp/Screens/Profile/SearchUsers/SearchUsersView.swift b/SwiftUI-WorkoutApp/Screens/Profile/SearchUsers/SearchUsersView.swift
index 4f27361b..1de982c2 100644
--- a/SwiftUI-WorkoutApp/Screens/Profile/SearchUsers/SearchUsersView.swift
+++ b/SwiftUI-WorkoutApp/Screens/Profile/SearchUsers/SearchUsersView.swift
@@ -21,11 +21,13 @@ struct SearchUsersView: View {
.onSubmit(search)
.submitLabel(.search)
.focused($isFocused)
+ .accessibilityIdentifier("SearchUserNameField")
}
Section("Результаты поиска") {
List(viewModel.users) { model in
listItem(for: model)
.disabled(model.id == defaults.mainUserInfo?.userID)
+ .accessibilityIdentifier("UserViewCell")
}
}
.opacity(viewModel.users.isEmpty ? 0 : 1)
diff --git a/SwiftUI-WorkoutApp/Screens/Profile/Settings/Login/LoginView.swift b/SwiftUI-WorkoutApp/Screens/Profile/Settings/Login/LoginView.swift
index 59302e1b..cfee033f 100644
--- a/SwiftUI-WorkoutApp/Screens/Profile/Settings/Login/LoginView.swift
+++ b/SwiftUI-WorkoutApp/Screens/Profile/Settings/Login/LoginView.swift
@@ -20,12 +20,8 @@ struct LoginView: View {
passwordField
}
Section {
- ButtonInForm("Войти", action: loginAction)
- .disabled(!viewModel.canLogIn)
- ButtonInForm("Забыли пароль?", mode: .secondary, action: forgotPasswordAction)
- .alert(Constants.Alert.forgotPassword, isPresented: $showResetInfoAlert) {
- Button("Ok") { viewModel.warningAlertClosed() }
- }
+ loginButton
+ forgotPasswordButton
}
}
.opacity(viewModel.isLoading ? 0.5 : 1)
@@ -63,6 +59,7 @@ private extension LoginView {
)
.focused($focus, equals: .username)
.onAppear(perform: showKeyboard)
+ .accessibilityIdentifier("loginField")
}
func showKeyboard() {
@@ -80,6 +77,20 @@ private extension LoginView {
)
.focused($focus, equals: .password)
.onSubmit(loginAction)
+ .accessibilityIdentifier("passwordField")
+ }
+
+ var loginButton: some View {
+ ButtonInForm("Войти", action: loginAction)
+ .disabled(!viewModel.canLogIn)
+ .accessibilityIdentifier("loginButton")
+ }
+
+ var forgotPasswordButton: some View {
+ ButtonInForm("Забыли пароль?", mode: .secondary, action: forgotPasswordAction)
+ .alert(Constants.Alert.forgotPassword, isPresented: $showResetInfoAlert) {
+ Button("Ok") { viewModel.warningAlertClosed() }
+ }
}
func loginAction() {
diff --git a/SwiftUI-WorkoutApp/Screens/SportsGrounds/List/SportsGroundsListView.swift b/SwiftUI-WorkoutApp/Screens/SportsGrounds/List/SportsGroundsListView.swift
index 704c7d6f..7706b66c 100644
--- a/SwiftUI-WorkoutApp/Screens/SportsGrounds/List/SportsGroundsListView.swift
+++ b/SwiftUI-WorkoutApp/Screens/SportsGrounds/List/SportsGroundsListView.swift
@@ -29,6 +29,7 @@ struct SportsGroundsListView: View {
} label: {
SportsGroundViewCell(model: ground)
}
+ .accessibilityIdentifier("SportsGroundViewCell")
default:
NavigationLink {
SportsGroundDetailView(
@@ -38,6 +39,7 @@ struct SportsGroundsListView: View {
} label: {
SportsGroundViewCell(model: ground)
}
+ .accessibilityIdentifier("SportsGroundViewCell")
}
}
.opacity(viewModel.isLoading ? 0.5 : 1)
diff --git a/SwiftUI-WorkoutApp/SwiftUI_WorkoutAppApp.swift b/SwiftUI-WorkoutApp/SwiftUI_WorkoutAppApp.swift
index 89db048d..13653e1e 100644
--- a/SwiftUI-WorkoutApp/SwiftUI_WorkoutAppApp.swift
+++ b/SwiftUI-WorkoutApp/SwiftUI_WorkoutAppApp.swift
@@ -8,10 +8,8 @@ struct SwiftUI_WorkoutAppApp: App {
@StateObject private var network = CheckNetworkService()
init() {
- UITextField.appearance().clearButtonMode = .whileEditing
- let tabBarAppearance = UITabBarAppearance()
- tabBarAppearance.configureWithOpaqueBackground()
- UITabBar.appearance().scrollEdgeAppearance = tabBarAppearance
+ setupAppearance()
+ prepareForUITestIfNeeded()
}
var body: some Scene {
@@ -31,3 +29,19 @@ struct SwiftUI_WorkoutAppApp: App {
}
}
}
+
+private extension SwiftUI_WorkoutAppApp {
+ func setupAppearance() {
+ UITextField.appearance().clearButtonMode = .whileEditing
+ let tabBarAppearance = UITabBarAppearance()
+ tabBarAppearance.configureWithOpaqueBackground()
+ UITabBar.appearance().scrollEdgeAppearance = tabBarAppearance
+ }
+
+ func prepareForUITestIfNeeded() {
+ if ProcessInfo.processInfo.arguments.contains("UITest") {
+ UserDefaults.standard.removePersistentDomain(forName: Bundle.main.bundleIdentifier!)
+ UIView.setAnimationsEnabled(false)
+ }
+ }
+}
diff --git a/WorkoutAppUITests/WorkoutAppUITests.swift b/WorkoutAppUITests/WorkoutAppUITests.swift
new file mode 100644
index 00000000..5a6eee70
--- /dev/null
+++ b/WorkoutAppUITests/WorkoutAppUITests.swift
@@ -0,0 +1,73 @@
+import XCTest
+
+final class WorkoutAppUITests: XCTestCase {
+ private let springBoard = XCUIApplication(bundleIdentifier: "com.apple.springboard")
+ private var app: XCUIApplication!
+
+ override func setUp() {
+ super.setUp()
+ continueAfterFailure = false
+ app = XCUIApplication()
+ app.launchArguments.append("UITest")
+ setupSnapshot(app)
+ app.launch()
+ }
+
+ override func tearDown() {
+ super.tearDown()
+ app.launchArguments.removeAll()
+ app = nil
+ }
+
+ func testMakeScreenshots() {
+ waitAndTap(timeout: 4, element: grantLocationAccessButton)
+ waitAndTapOrFail(timeout: 4, element: profileTabButton)
+ waitAndTapOrFail(element: authorizeButton)
+ waitAndTapOrFail(element: loginField)
+ loginField.typeText(Constants.login)
+ waitAndTapOrFail(element: passwordField)
+ passwordField.typeText(Constants.password)
+ waitAndTapOrFail(element: loginButton)
+ waitAndTapOrFail(timeout: 4, element: searchUsersButton)
+ waitAndTapOrFail(element: searchUserField)
+ searchUserField.typeText(Constants.usernameForSearch)
+ searchUserField.typeText("\n") // жмем "return", чтобы начать поиск
+ waitAndTapOrFail(timeout: 4, element: firstFoundUserCell)
+ snapshot("1-profile", timeWaitingForIdle: 3)
+ swipeToFind(element: usesSportsGroundsButton, in: app)
+ waitAndTapOrFail(timeout: 4, element: firstSportsGroundCell)
+ snapshot("2-sportsGroundDetails", timeWaitingForIdle: 3)
+ waitAndTapOrFail(element: eventsTabButton)
+ waitAndTapOrFail(element: pastEventsPickerButton)
+ snapshot("3-pastEvents", timeWaitingForIdle: 3)
+ waitAndTapOrFail(element: firstEventViewCell)
+ snapshot("4-eventDetails", timeWaitingForIdle: 3)
+ }
+}
+
+private extension WorkoutAppUITests {
+ enum Constants {
+ static let login = "testuserapple"
+ static let password = "111111"
+ static let usernameForSearch = "Ninenineone"
+ }
+
+ var grantLocationAccessButton: XCUIElement { springBoard.alerts.firstMatch.buttons["При использовании"] }
+ var pasteButton: XCUIElement { app.menuItems["Вставить"] }
+ var tabbar: XCUIElement { app.tabBars["Панель вкладок"] }
+ var profileTabButton: XCUIElement { tabbar.buttons["Профиль"] }
+ var eventsTabButton: XCUIElement { tabbar.buttons["Мероприятия"] }
+ var authorizeButton: XCUIElement { app.buttons["Авторизация"] }
+ var loginField: XCUIElement { app.textFields["loginField"] }
+ var passwordField: XCUIElement { app.secureTextFields["passwordField"] }
+ var loginButton: XCUIElement { app.buttons["loginButton"] }
+ var profileNavBar: XCUIElement { app.navigationBars["Профиль"] }
+ var searchUsersButton: XCUIElement { profileNavBar.buttons["searchUsersButton"] }
+ var searchUserField: XCUIElement { app.textFields["SearchUserNameField"] }
+ var keyboardSearchButton: XCUIElement { app.keyboards.buttons["Search"] }
+ var firstFoundUserCell: XCUIElement { app.buttons["UserViewCell"].firstMatch }
+ var usesSportsGroundsButton: XCUIElement { app.buttons["usesSportsGroundsButton"] }
+ var firstSportsGroundCell: XCUIElement { app.buttons["SportsGroundViewCell"].firstMatch }
+ var pastEventsPickerButton: XCUIElement { app.segmentedControls.firstMatch.buttons["Прошедшие"] }
+ var firstEventViewCell: XCUIElement { app.buttons["EventViewCell"].firstMatch }
+}
diff --git a/WorkoutAppUITests/XCTestCase+.swift b/WorkoutAppUITests/XCTestCase+.swift
new file mode 100644
index 00000000..09cea0fd
--- /dev/null
+++ b/WorkoutAppUITests/XCTestCase+.swift
@@ -0,0 +1,46 @@
+import XCTest
+
+extension XCTestCase {
+ @discardableResult
+ func waitAndTap(timeout: TimeInterval, element: XCUIElement) -> Bool {
+ let isElementFound = element.waitForExistence(timeout: timeout)
+ if isElementFound { element.tapElement() }
+ return isElementFound
+ }
+
+ func waitAndTapOrFail(timeout: TimeInterval = 3, element: XCUIElement) {
+ if !waitAndTap(timeout: timeout, element: element) {
+ XCTFail("Не нашли элемент \(element)")
+ }
+ }
+
+ func waitAndPressOrFail(timeout: TimeInterval = 3, pressDuration: TimeInterval = 1.1, element: XCUIElement) {
+ if element.waitForExistence(timeout: timeout) {
+ element.press(forDuration: pressDuration)
+ } else {
+ XCTFail("Не нашли элемент \(element)")
+ }
+ }
+
+ func swipeToFind(element: XCUIElement, in app: XCUIApplication, direction: SwipeDirection = .up) {
+ while !element.isVisibleOnScreen {
+ switch direction {
+ case .up:
+ app.swipeUp(velocity: .slow)
+ case .down:
+ app.swipeDown(velocity: .slow)
+ case .left:
+ app.swipeLeft(velocity: .slow)
+ case .right:
+ app.swipeRight(velocity: .slow)
+ }
+ }
+ waitAndTapOrFail(element: element)
+ }
+}
+
+extension XCTestCase {
+ enum SwipeDirection {
+ case up, down, left, right
+ }
+}
diff --git a/WorkoutAppUITests/XCUIElement+.swift b/WorkoutAppUITests/XCUIElement+.swift
new file mode 100644
index 00000000..016e8e17
--- /dev/null
+++ b/WorkoutAppUITests/XCUIElement+.swift
@@ -0,0 +1,15 @@
+import XCTest
+
+extension XCUIElement {
+ func tapElement() {
+ if isHittable {
+ tap()
+ } else {
+ coordinate(withNormalizedOffset: .init(dx: 0.0, dy: 0.0)).tap()
+ }
+ }
+
+ var isVisibleOnScreen: Bool {
+ exists && isHittable
+ }
+}
diff --git a/screenshots/ru/iPhone 13 Pro Max-1-profile.png b/screenshots/ru/iPhone 13 Pro Max-1-profile.png
new file mode 100644
index 00000000..d6b6fb74
Binary files /dev/null and b/screenshots/ru/iPhone 13 Pro Max-1-profile.png differ
diff --git a/screenshots/ru/iPhone 13 Pro Max-2-sportsGroundDetails.png b/screenshots/ru/iPhone 13 Pro Max-2-sportsGroundDetails.png
new file mode 100644
index 00000000..a0626e0f
Binary files /dev/null and b/screenshots/ru/iPhone 13 Pro Max-2-sportsGroundDetails.png differ
diff --git a/screenshots/ru/iPhone 13 Pro Max-3-pastEvents.png b/screenshots/ru/iPhone 13 Pro Max-3-pastEvents.png
new file mode 100644
index 00000000..05ec3c4c
Binary files /dev/null and b/screenshots/ru/iPhone 13 Pro Max-3-pastEvents.png differ
diff --git a/screenshots/ru/iPhone 13 Pro Max-4-eventDetails.png b/screenshots/ru/iPhone 13 Pro Max-4-eventDetails.png
new file mode 100644
index 00000000..59853105
Binary files /dev/null and b/screenshots/ru/iPhone 13 Pro Max-4-eventDetails.png differ
diff --git a/screenshots/ru/iPhone 13 Pro-1-profile.png b/screenshots/ru/iPhone 13 Pro-1-profile.png
new file mode 100644
index 00000000..90475434
Binary files /dev/null and b/screenshots/ru/iPhone 13 Pro-1-profile.png differ
diff --git a/screenshots/ru/iPhone 13 Pro-2-sportsGroundDetails.png b/screenshots/ru/iPhone 13 Pro-2-sportsGroundDetails.png
new file mode 100644
index 00000000..842d6861
Binary files /dev/null and b/screenshots/ru/iPhone 13 Pro-2-sportsGroundDetails.png differ
diff --git a/screenshots/ru/iPhone 13 Pro-3-pastEvents.png b/screenshots/ru/iPhone 13 Pro-3-pastEvents.png
new file mode 100644
index 00000000..42941320
Binary files /dev/null and b/screenshots/ru/iPhone 13 Pro-3-pastEvents.png differ
diff --git a/screenshots/ru/iPhone 13 Pro-4-eventDetails.png b/screenshots/ru/iPhone 13 Pro-4-eventDetails.png
new file mode 100644
index 00000000..78d6b23b
Binary files /dev/null and b/screenshots/ru/iPhone 13 Pro-4-eventDetails.png differ
diff --git a/screenshots/ru/iPhone 8 Plus-1-profile.png b/screenshots/ru/iPhone 8 Plus-1-profile.png
new file mode 100644
index 00000000..5ff843cc
Binary files /dev/null and b/screenshots/ru/iPhone 8 Plus-1-profile.png differ
diff --git a/screenshots/ru/iPhone 8 Plus-2-sportsGroundDetails.png b/screenshots/ru/iPhone 8 Plus-2-sportsGroundDetails.png
new file mode 100644
index 00000000..700c6ed8
Binary files /dev/null and b/screenshots/ru/iPhone 8 Plus-2-sportsGroundDetails.png differ
diff --git a/screenshots/ru/iPhone 8 Plus-3-pastEvents.png b/screenshots/ru/iPhone 8 Plus-3-pastEvents.png
new file mode 100644
index 00000000..c571c201
Binary files /dev/null and b/screenshots/ru/iPhone 8 Plus-3-pastEvents.png differ
diff --git a/screenshots/ru/iPhone 8 Plus-4-eventDetails.png b/screenshots/ru/iPhone 8 Plus-4-eventDetails.png
new file mode 100644
index 00000000..d47ab8c7
Binary files /dev/null and b/screenshots/ru/iPhone 8 Plus-4-eventDetails.png differ
diff --git a/screenshots/ru/iPhone SE (3rd generation)-1-profile.png b/screenshots/ru/iPhone SE (3rd generation)-1-profile.png
new file mode 100644
index 00000000..bd2c981e
Binary files /dev/null and b/screenshots/ru/iPhone SE (3rd generation)-1-profile.png differ
diff --git a/screenshots/ru/iPhone SE (3rd generation)-2-sportsGroundDetails.png b/screenshots/ru/iPhone SE (3rd generation)-2-sportsGroundDetails.png
new file mode 100644
index 00000000..eaaf49b9
Binary files /dev/null and b/screenshots/ru/iPhone SE (3rd generation)-2-sportsGroundDetails.png differ
diff --git a/screenshots/ru/iPhone SE (3rd generation)-3-pastEvents.png b/screenshots/ru/iPhone SE (3rd generation)-3-pastEvents.png
new file mode 100644
index 00000000..b7c2a083
Binary files /dev/null and b/screenshots/ru/iPhone SE (3rd generation)-3-pastEvents.png differ
diff --git a/screenshots/ru/iPhone SE (3rd generation)-4-eventDetails.png b/screenshots/ru/iPhone SE (3rd generation)-4-eventDetails.png
new file mode 100644
index 00000000..b0660b56
Binary files /dev/null and b/screenshots/ru/iPhone SE (3rd generation)-4-eventDetails.png differ
diff --git a/screenshots/screenshots.html b/screenshots/screenshots.html
new file mode 100644
index 00000000..349f234f
--- /dev/null
+++ b/screenshots/screenshots.html
@@ -0,0 +1,604 @@
+
+
+
+ fastlane/snapshot
+
+
+
+
+
+
+ By Screen:
+
1-profile
+
+
+
2-sportsGroundDetails
+
+
+
3-pastEvents
+
+
+
4-eventDetails
+
+
+
iPhone SE (3rd generation)-1-profile
+
+
+
iPhone SE (3rd generation)-2-sportsGroundDetails
+
+
+
iPhone SE (3rd generation)-3-pastEvents
+
+
+
iPhone SE (3rd generation)-4-eventDetails
+
+
+
+
+
![]()
+
+
+
+
+