Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1185d0ce61 | ||
|
|
90d481a2d1 | ||
|
|
50b8a8c8e2 |
@@ -7,6 +7,7 @@
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
8F7E6D5C4B3A29180796A5B4 /* HubFlowsUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C8B7A6D5E4F3A2B1C0D9E8F /* HubFlowsUITests.swift */; };
|
||||
06FE9996B295456D8B8C7E96 /* BroadcastPermissions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BFB2F18615747B1A1C9E9B2 /* BroadcastPermissions.swift */; };
|
||||
095654DAAF6C4EA4B73BAB61 /* LiveScoreDialogHostTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4F8BF2514BB4F5185827B95 /* LiveScoreDialogHostTests.swift */; };
|
||||
0CB8933AC2B34D4099BBA881 /* MatchStatusBadge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34BB2251A67E401A9F1E789E /* MatchStatusBadge.swift */; };
|
||||
@@ -107,9 +108,18 @@
|
||||
remoteGlobalIDString = F54F6C97361C4E8A96AEDD11;
|
||||
remoteInfo = MatchLiveTv;
|
||||
};
|
||||
4B3A29180796A5B4C3D2E1F0 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = AE0076753A1840A19E9F565F /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = F54F6C97361C4E8A96AEDD11;
|
||||
remoteInfo = MatchLiveTv;
|
||||
};
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
9C8B7A6D5E4F3A2B1C0D9E8F /* HubFlowsUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchLiveTvUITests/HubFlowsUITests.swift; sourceTree = "<group>"; };
|
||||
7E6D5C4B3A29180796A5B4C3 /* MatchLiveTvUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MatchLiveTvUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
004F86D8E8BE47E589B923FA /* KeepScreenOn.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = KeepScreenOn.swift; path = MatchLiveTv/UI/System/KeepScreenOn.swift; sourceTree = "<group>"; };
|
||||
1079231B9A4E446AA9AD6C0A /* StepNetworkTestScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = StepNetworkTestScreen.swift; path = MatchLiveTv/UI/Wizard/StepNetworkTestScreen.swift; sourceTree = "<group>"; };
|
||||
10AD0F18190542818572CAEF /* ScoreActionDecodeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ScoreActionDecodeTests.swift; path = MatchLiveTvTests/ScoreActionDecodeTests.swift; sourceTree = "<group>"; };
|
||||
@@ -228,6 +238,7 @@
|
||||
children = (
|
||||
CE0F9D7D5EE749E8AAE6AB43 /* MatchLiveTv.app */,
|
||||
3FC0CC8155BE469ABB420DC5 /* MatchLiveTvTests.xctest */,
|
||||
7E6D5C4B3A29180796A5B4C3 /* MatchLiveTvUITests.xctest */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
@@ -248,11 +259,20 @@
|
||||
name = MatchLiveTvTests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
2A180796A5B4C3D2E1F0A98B /* MatchLiveTvUITests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
9C8B7A6D5E4F3A2B1C0D9E8F /* HubFlowsUITests.swift */,
|
||||
);
|
||||
name = MatchLiveTvUITests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
7E992B84100649D8BE8507F7 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DDEF729FE3334F8998C4A31C /* MatchLiveTv */,
|
||||
6987D3982FAD44A6869B9FD6 /* MatchLiveTvTests */,
|
||||
2A180796A5B4C3D2E1F0A98B /* MatchLiveTvUITests */,
|
||||
0ADC6126267C410F9B60B426 /* Products */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
@@ -364,6 +384,22 @@
|
||||
productReference = 3FC0CC8155BE469ABB420DC5 /* MatchLiveTvTests.xctest */;
|
||||
productType = "com.apple.product-type.bundle.unit-test";
|
||||
};
|
||||
6D5C4B3A29180796A5B4C3D2 /* MatchLiveTvUITests */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = F01796A5B4C3D2E1F0A98D7E /* Build configuration list for PBXNativeTarget "MatchLiveTvUITests" */;
|
||||
buildPhases = (
|
||||
5C4B3A29180796A5B4C3D2E1 /* Sources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
3A29180796A5B4C3D2E1F0A9 /* PBXTargetDependency */,
|
||||
);
|
||||
name = MatchLiveTvUITests;
|
||||
productName = MatchLiveTvUITests;
|
||||
productReference = 7E6D5C4B3A29180796A5B4C3 /* MatchLiveTvUITests.xctest */;
|
||||
productType = "com.apple.product-type.bundle.ui-testing";
|
||||
};
|
||||
F54F6C97361C4E8A96AEDD11 /* MatchLiveTv */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 117DCD84E04B4B03A2BFFFEA /* Build configuration list for PBXNativeTarget "MatchLiveTv" */;
|
||||
@@ -414,6 +450,7 @@
|
||||
targets = (
|
||||
F54F6C97361C4E8A96AEDD11 /* MatchLiveTv */,
|
||||
20C695DBFB4042879308B0F5 /* MatchLiveTvTests */,
|
||||
6D5C4B3A29180796A5B4C3D2 /* MatchLiveTvUITests */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
@@ -531,6 +568,14 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
5C4B3A29180796A5B4C3D2E1 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
8F7E6D5C4B3A29180796A5B4 /* HubFlowsUITests.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXTargetDependency section */
|
||||
@@ -539,6 +584,11 @@
|
||||
target = F54F6C97361C4E8A96AEDD11 /* MatchLiveTv */;
|
||||
targetProxy = 58E3C5EC752E497FBFD32D78 /* PBXContainerItemProxy */;
|
||||
};
|
||||
3A29180796A5B4C3D2E1F0A9 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = F54F6C97361C4E8A96AEDD11 /* MatchLiveTv */;
|
||||
targetProxy = 4B3A29180796A5B4C3D2E1F0 /* PBXContainerItemProxy */;
|
||||
};
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
@@ -627,10 +677,11 @@
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 32;
|
||||
CURRENT_PROJECT_VERSION = 35;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_KEY_API_BASE_URL = "$(API_BASE_URL)";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
|
||||
MARKETING_VERSION = 2.0.10;
|
||||
MARKETING_VERSION = 2.0.12;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.matchlivetv.match-live-tv.tests";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
@@ -647,7 +698,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 32;
|
||||
CURRENT_PROJECT_VERSION = 35;
|
||||
DEVELOPMENT_TEAM = S8Q9TWBRG5;
|
||||
GENERATE_INFOPLIST_FILE = NO;
|
||||
INFOPLIST_FILE = MatchLiveTv/Resources/Info.plist;
|
||||
@@ -658,7 +709,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.0.10;
|
||||
MARKETING_VERSION = 2.0.12;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.matchlivetv.match-live-tv";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
@@ -676,7 +727,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 32;
|
||||
CURRENT_PROJECT_VERSION = 35;
|
||||
DEVELOPMENT_TEAM = S8Q9TWBRG5;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
GENERATE_INFOPLIST_FILE = NO;
|
||||
@@ -688,7 +739,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.0.10;
|
||||
MARKETING_VERSION = 2.0.12;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.matchlivetv.match-live-tv";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
@@ -705,10 +756,11 @@
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 32;
|
||||
CURRENT_PROJECT_VERSION = 35;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_KEY_API_BASE_URL = "$(API_BASE_URL)";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
|
||||
MARKETING_VERSION = 2.0.10;
|
||||
MARKETING_VERSION = 2.0.12;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.matchlivetv.match-live-tv.tests";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
@@ -718,6 +770,44 @@
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
19180796A5B4C3D2E1F0A98C /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 35;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_KEY_API_BASE_URL = "$(API_BASE_URL)";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
|
||||
MARKETING_VERSION = 2.0.12;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.matchlivetv.match-live-tv.uitests";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_TARGET_NAME = MatchLiveTv;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
081796A5B4C3D2E1F0A98D7E /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 35;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_KEY_API_BASE_URL = "$(API_BASE_URL)";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
|
||||
MARKETING_VERSION = 2.0.12;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.matchlivetv.match-live-tv.uitests";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_TARGET_NAME = MatchLiveTv;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
@@ -748,6 +838,15 @@
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
F01796A5B4C3D2E1F0A98D7E /* Build configuration list for PBXNativeTarget "MatchLiveTvUITests" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
19180796A5B4C3D2E1F0A98C /* Debug */,
|
||||
081796A5B4C3D2E1F0A98D7E /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
|
||||
/* Begin XCRemoteSwiftPackageReference section */
|
||||
|
||||
@@ -52,6 +52,16 @@
|
||||
ReferencedContainer = "container:MatchLiveTv.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "6D5C4B3A29180796A5B4C3D2"
|
||||
BuildableName = "MatchLiveTvUITests.xctest"
|
||||
BlueprintName = "MatchLiveTvUITests"
|
||||
ReferencedContainer = "container:MatchLiveTv.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
|
||||
@@ -2,6 +2,10 @@ import Foundation
|
||||
|
||||
enum AppConfig {
|
||||
static var apiBaseUrl: String {
|
||||
if let env = ProcessInfo.processInfo.environment["API_BASE_URL"]?.trimmingCharacters(in: CharacterSet(charactersIn: "/ ")),
|
||||
!env.isEmpty {
|
||||
return env
|
||||
}
|
||||
let raw = Bundle.main.object(forInfoDictionaryKey: "API_BASE_URL") as? String
|
||||
?? "https://www.matchlivetv.it"
|
||||
return raw.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
|
||||
|
||||
@@ -1763,6 +1763,7 @@ struct LanguagePickerView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.accessibilityIdentifier("language.\(lang.rawValue)")
|
||||
}
|
||||
}
|
||||
.navigationTitle(L10n.t("language.label"))
|
||||
|
||||
@@ -16,8 +16,21 @@ final class TokenStore: ObservableObject {
|
||||
private let service = "com.matchlivetv.match-live-tv.auth"
|
||||
|
||||
init() {
|
||||
session = loadSession()
|
||||
activeTeamId = defaults.string(forKey: Keys.activeTeamId)
|
||||
if ProcessInfo.processInfo.arguments.contains("--uitesting-reset") {
|
||||
deleteKeychain(key: Keys.accessToken)
|
||||
deleteKeychain(key: Keys.refreshToken)
|
||||
defaults.removeObject(forKey: Keys.userId)
|
||||
defaults.removeObject(forKey: Keys.userEmail)
|
||||
defaults.removeObject(forKey: Keys.userName)
|
||||
defaults.removeObject(forKey: Keys.userRole)
|
||||
defaults.removeObject(forKey: Keys.activeTeamId)
|
||||
defaults.set("it", forKey: "mltv.appLanguage")
|
||||
session = nil
|
||||
activeTeamId = nil
|
||||
} else {
|
||||
session = loadSession()
|
||||
activeTeamId = defaults.string(forKey: Keys.activeTeamId)
|
||||
}
|
||||
}
|
||||
|
||||
func saveSession(_ session: StoredSession) {
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>2.0.10</string>
|
||||
<string>2.0.12</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
@@ -32,7 +32,7 @@
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>32</string>
|
||||
<string>35</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
@@ -41,10 +41,6 @@
|
||||
<string>Match Live TV usa il microfono per l'audio della diretta.</string>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>Match Live TV usa le foto per i loghi delle squadre.</string>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>audio</string>
|
||||
</array>
|
||||
<key>UILaunchScreen</key>
|
||||
<dict>
|
||||
<key>UIColorName</key>
|
||||
|
||||
@@ -30,9 +30,11 @@ struct AccountScreen: View {
|
||||
Image(systemName: "chevron.left")
|
||||
.foregroundStyle(MatchColors.textSecondary)
|
||||
}
|
||||
.accessibilityIdentifier("account.back")
|
||||
Text(L10n.t("account.title"))
|
||||
.font(MatchTypography.headlineMedium)
|
||||
.foregroundStyle(.white)
|
||||
.accessibilityIdentifier("account.title")
|
||||
Spacer()
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
@@ -66,6 +68,7 @@ struct AccountScreen: View {
|
||||
enabled: !loadingProfile && !name.trimmingCharacters(in: .whitespaces).isEmpty && !savingProfile,
|
||||
loading: savingProfile
|
||||
)
|
||||
.accessibilityIdentifier("account.save.profile")
|
||||
|
||||
Text(L10n.t("account.password.heading"))
|
||||
.font(MatchTypography.titleMedium)
|
||||
@@ -75,17 +78,20 @@ struct AccountScreen: View {
|
||||
MatchAccountSecureField(
|
||||
title: L10n.t("account.current.password"),
|
||||
text: $currentPassword,
|
||||
visible: $passwordVisible
|
||||
visible: $passwordVisible,
|
||||
accessibilityId: "account.password.current"
|
||||
)
|
||||
MatchAccountSecureField(
|
||||
title: L10n.t("account.new.password"),
|
||||
text: $newPassword,
|
||||
visible: $passwordVisible
|
||||
visible: $passwordVisible,
|
||||
accessibilityId: "account.password.new"
|
||||
)
|
||||
MatchAccountSecureField(
|
||||
title: L10n.t("account.confirm.password"),
|
||||
text: $confirmPassword,
|
||||
visible: $passwordVisible
|
||||
visible: $passwordVisible,
|
||||
accessibilityId: "account.password.confirm"
|
||||
)
|
||||
|
||||
if let passwordError {
|
||||
@@ -101,12 +107,14 @@ struct AccountScreen: View {
|
||||
enabled: !currentPassword.isEmpty && !newPassword.isEmpty && !confirmPassword.isEmpty && !savingPassword,
|
||||
loading: savingPassword
|
||||
)
|
||||
.accessibilityIdentifier("account.save.password")
|
||||
|
||||
MatchSecondaryButton(
|
||||
label: L10n.t("action.logout"),
|
||||
action: logout,
|
||||
enabled: !loggingOut
|
||||
)
|
||||
.accessibilityIdentifier("account.logout")
|
||||
.padding(.top, 24)
|
||||
}
|
||||
.padding(24)
|
||||
@@ -214,6 +222,8 @@ private struct MatchAccountSecureField: View {
|
||||
@Binding var text: String
|
||||
@Binding var visible: Bool
|
||||
|
||||
var accessibilityId: String = ""
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text(title)
|
||||
@@ -229,6 +239,7 @@ private struct MatchAccountSecureField: View {
|
||||
}
|
||||
.textInputAutocapitalization(.never)
|
||||
.autocorrectionDisabled()
|
||||
.accessibilityIdentifier(accessibilityId)
|
||||
Button(action: { visible.toggle() }) {
|
||||
Image(systemName: visible ? "eye.slash" : "eye")
|
||||
.foregroundStyle(MatchColors.textSecondary)
|
||||
|
||||
@@ -194,16 +194,19 @@ struct BroadcastControlsOverlay: View {
|
||||
action: { pendingConfirm = .shareLive },
|
||||
enabled: shareLiveEnabled
|
||||
)
|
||||
.accessibilityIdentifier("broadcast.share")
|
||||
SideIconButton(
|
||||
systemName: "video.fill",
|
||||
accessibilityLabel: L10n.t("broadcast.share.regia.cd"),
|
||||
action: { pendingConfirm = .shareRegia }
|
||||
)
|
||||
.accessibilityIdentifier("broadcast.regia")
|
||||
SideIconButton(
|
||||
systemName: "slider.horizontal.3",
|
||||
accessibilityLabel: L10n.t("broadcast.min.quality.cd"),
|
||||
action: { showMinQualityPicker = true }
|
||||
)
|
||||
.accessibilityIdentifier("broadcast.abr")
|
||||
if let onCloseSet {
|
||||
SideIconButton(
|
||||
systemName: "checkmark",
|
||||
@@ -231,18 +234,21 @@ struct BroadcastControlsOverlay: View {
|
||||
action: { pendingConfirm = .pauseOrResume },
|
||||
highlighted: isPaused
|
||||
)
|
||||
.accessibilityIdentifier("broadcast.pause")
|
||||
SideIconButton(
|
||||
systemName: audioMuted ? "speaker.slash.fill" : "speaker.wave.2.fill",
|
||||
accessibilityLabel: audioMuted ? L10n.t("broadcast.unmute.cd") : L10n.t("broadcast.mute.cd"),
|
||||
action: { pendingConfirm = .toggleMute },
|
||||
highlighted: audioMuted
|
||||
)
|
||||
.accessibilityIdentifier("broadcast.mute")
|
||||
SideIconButton(
|
||||
systemName: "stop.fill",
|
||||
accessibilityLabel: L10n.t("broadcast.terminate.cd"),
|
||||
action: { pendingConfirm = .terminate },
|
||||
danger: true
|
||||
)
|
||||
.accessibilityIdentifier("broadcast.terminate")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -510,10 +516,12 @@ private struct TeamScoreColumn: View {
|
||||
}
|
||||
}
|
||||
ScoreIconButton(label: "+1", tooltip: L10n.t("broadcast.tooltip.add.point", teamSide), action: onPlus, primary: !showBasketButtons)
|
||||
.accessibilityIdentifier("broadcast.score.away.plus")
|
||||
ScoreIconButton(label: "−", tooltip: L10n.t("broadcast.tooltip.remove.point", teamSide), action: onMinus)
|
||||
} else {
|
||||
ScoreIconButton(label: "−", tooltip: L10n.t("broadcast.tooltip.remove.point", teamSide), action: onMinus)
|
||||
ScoreIconButton(label: "+1", tooltip: L10n.t("broadcast.tooltip.add.point", teamSide), action: onPlus, primary: !showBasketButtons)
|
||||
.accessibilityIdentifier(alignEnd ? "broadcast.score.away.plus" : "broadcast.score.home.plus")
|
||||
if showBasketButtons {
|
||||
if let onPlus2 {
|
||||
ScoreIconButton(label: "+2", tooltip: L10n.t("broadcast.tooltip.plus.side", 2, teamSide), action: onPlus2, primary: true)
|
||||
|
||||
@@ -18,6 +18,7 @@ struct ForgotPasswordScreen: View {
|
||||
Image(systemName: "chevron.left")
|
||||
.foregroundStyle(MatchColors.textSecondary)
|
||||
}
|
||||
.accessibilityIdentifier("forgot.back")
|
||||
Text(L10n.t("forgot.password.title"))
|
||||
.font(MatchTypography.headlineMedium)
|
||||
.foregroundStyle(.white)
|
||||
@@ -64,6 +65,7 @@ struct ForgotPasswordScreen: View {
|
||||
enabled: !email.trimmingCharacters(in: .whitespaces).isEmpty && !loading && !success,
|
||||
loading: loading
|
||||
)
|
||||
.accessibilityIdentifier("forgot.submit")
|
||||
}
|
||||
.padding(24)
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ struct LoginScreen: View {
|
||||
.foregroundStyle(MatchColors.textSecondary)
|
||||
}
|
||||
.accessibilityLabel(L10n.t("language.label"))
|
||||
.accessibilityIdentifier("login.language")
|
||||
}
|
||||
.padding(.top, 8)
|
||||
MatchLiveWordmark(showSlogan: true)
|
||||
@@ -57,6 +58,7 @@ struct LoginScreen: View {
|
||||
.foregroundStyle(MatchColors.textSecondary)
|
||||
.underline()
|
||||
}
|
||||
.accessibilityIdentifier("login.forgot")
|
||||
}
|
||||
.padding(.top, 12)
|
||||
if let error {
|
||||
@@ -71,6 +73,7 @@ struct LoginScreen: View {
|
||||
enabled: !email.isEmpty && !password.isEmpty,
|
||||
loading: loading
|
||||
)
|
||||
.accessibilityIdentifier("login.submit")
|
||||
.padding(.top, 32)
|
||||
}
|
||||
.padding(.horizontal, 24)
|
||||
|
||||
@@ -18,6 +18,8 @@ struct MatchesScreen: View {
|
||||
@State private var showSchedule = false
|
||||
@State private var showTeamPicker = false
|
||||
@State private var resumeMatch: Match?
|
||||
@State private var configureMatch: Match?
|
||||
@State private var pendingConfigureMatch: Match?
|
||||
@State private var deleteMatch: Match?
|
||||
@State private var snackbar: String?
|
||||
@State private var showLanguagePicker = false
|
||||
@@ -35,15 +37,19 @@ struct MatchesScreen: View {
|
||||
} label: {
|
||||
Image(systemName: "globe")
|
||||
.foregroundStyle(MatchColors.textSecondary)
|
||||
.frame(minWidth: 44, minHeight: 44)
|
||||
}
|
||||
.accessibilityLabel(L10n.t("language.label"))
|
||||
.accessibilityIdentifier("matches.language")
|
||||
Button {
|
||||
onOpenAccount()
|
||||
} label: {
|
||||
Image(systemName: "person.crop.circle")
|
||||
.foregroundStyle(MatchColors.textSecondary)
|
||||
.frame(minWidth: 44, minHeight: 44)
|
||||
}
|
||||
.accessibilityLabel(L10n.t("account.title"))
|
||||
.accessibilityIdentifier("matches.account")
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 8)
|
||||
@@ -91,6 +97,7 @@ struct MatchesScreen: View {
|
||||
.font(MatchTypography.bodyMedium)
|
||||
.foregroundStyle(MatchColors.textSecondary)
|
||||
MatchPrimaryButton(label: L10n.t("matches.new").uppercased(), action: { showNewMatch = true })
|
||||
.accessibilityIdentifier("matches.new")
|
||||
if let activeTeam {
|
||||
TeamPickerBar(
|
||||
team: activeTeam,
|
||||
@@ -151,14 +158,16 @@ struct MatchesScreen: View {
|
||||
}
|
||||
)
|
||||
.task(id: refreshToken) { reload(showSpinner: refreshToken == 0) }
|
||||
.alert(L10n.t("sheet.configure.now.title"), isPresented: Binding(get: { resumeMatch != nil }, set: { if !$0 { resumeMatch = nil } })) {
|
||||
Button(L10n.t("sheet.resume.camera")) {
|
||||
if let match = resumeMatch { resumeBroadcast(match) }
|
||||
resumeMatch = nil
|
||||
.alert(
|
||||
L10n.t("sheet.configure.now.title"),
|
||||
isPresented: Binding(get: { configureMatch != nil }, set: { if !$0 { configureMatch = nil } })
|
||||
) {
|
||||
Button(L10n.t("sheet.configure")) {
|
||||
if let match = configureMatch { onOpenSetup(match.id) }
|
||||
configureMatch = nil
|
||||
}
|
||||
Button(L10n.t("sheet.configure"), role: .cancel) {
|
||||
if let match = resumeMatch { onOpenSetup(match.id) }
|
||||
resumeMatch = nil
|
||||
Button(L10n.t("sheet.later"), role: .cancel) {
|
||||
configureMatch = nil
|
||||
}
|
||||
} message: {
|
||||
Text(L10n.t("sheet.configure.now.body"))
|
||||
@@ -201,10 +210,19 @@ struct MatchesScreen: View {
|
||||
.presentationDetents([.medium])
|
||||
.presentationDragIndicator(.visible)
|
||||
}
|
||||
.sheet(isPresented: $showSchedule) {
|
||||
.sheet(isPresented: $showSchedule, onDismiss: {
|
||||
if let match = pendingConfigureMatch {
|
||||
pendingConfigureMatch = nil
|
||||
configureMatch = match
|
||||
}
|
||||
}) {
|
||||
ScheduleMatchSheet(container: container, teamId: activeTeam?.id) { match in
|
||||
pendingConfigureMatch = match
|
||||
snackbar = L10n.t("matches.msg.scheduled.ok")
|
||||
reload(showSpinner: false)
|
||||
showSchedule = false
|
||||
onOpenSetup(match.id)
|
||||
} onFailed: { error in
|
||||
snackbar = UserFacingError.message(for: error) ?? L10n.t("matches.msg.schedule.error")
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showTeamPicker) {
|
||||
@@ -229,6 +247,29 @@ struct MatchesScreen: View {
|
||||
ProgressView().tint(MatchColors.primaryRed)
|
||||
}
|
||||
}
|
||||
.overlay {
|
||||
if let match = resumeMatch {
|
||||
ZStack(alignment: .bottom) {
|
||||
Color.black.opacity(0.4)
|
||||
.ignoresSafeArea()
|
||||
.onTapGesture { resumeMatch = nil }
|
||||
ResumeSessionSheet(
|
||||
match: match,
|
||||
onResumeCamera: {
|
||||
resumeMatch = nil
|
||||
resumeBroadcast(match)
|
||||
},
|
||||
onContinueSetup: {
|
||||
resumeMatch = nil
|
||||
onOpenSetup(match.id)
|
||||
},
|
||||
onDismiss: { resumeMatch = nil }
|
||||
)
|
||||
}
|
||||
.ignoresSafeArea(edges: .bottom)
|
||||
.accessibilityIdentifier("resume.sheet")
|
||||
}
|
||||
}
|
||||
.overlay(alignment: .bottom) {
|
||||
if let snackbar {
|
||||
Text(snackbar)
|
||||
@@ -379,6 +420,7 @@ private struct ActiveSessionBanner: View {
|
||||
.background(MatchColors.primaryRed.opacity(0.15), in: RoundedRectangle(cornerRadius: 12))
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityIdentifier("matches.resume.banner")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -523,12 +565,14 @@ private struct NewMatchSheet: View {
|
||||
subtitle: L10n.t("sheet.schedule.option.sub"),
|
||||
action: onSchedule
|
||||
)
|
||||
.accessibilityIdentifier("sheet.schedule.option")
|
||||
SheetOptionTile(
|
||||
systemImage: "play.circle",
|
||||
title: L10n.t("sheet.quick.option"),
|
||||
subtitle: L10n.t("sheet.quick.option.sub"),
|
||||
action: onQuickStart
|
||||
)
|
||||
.accessibilityIdentifier("sheet.quick.option")
|
||||
}
|
||||
.padding(.top, 20)
|
||||
Spacer(minLength: 24)
|
||||
@@ -570,14 +614,68 @@ private struct SheetOptionTile: View {
|
||||
}
|
||||
}
|
||||
|
||||
private struct ResumeSessionSheet: View {
|
||||
let match: Match
|
||||
let onResumeCamera: () -> Void
|
||||
let onContinueSetup: () -> Void
|
||||
let onDismiss: () -> Void
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
Text(L10n.t("sheet.live.in.progress"))
|
||||
.font(MatchTypography.headlineMedium)
|
||||
.accessibilityIdentifier("resume.sheet.title")
|
||||
Text("\(match.teamName) vs \(match.opponentName)")
|
||||
.font(MatchTypography.bodyMedium)
|
||||
.padding(.top, 4)
|
||||
if let status = match.activeSessionStatus, !status.isEmpty {
|
||||
Text(L10n.t("sheet.status", status))
|
||||
.font(MatchTypography.labelLarge)
|
||||
.foregroundStyle(MatchColors.primaryRed)
|
||||
.padding(.top, 8)
|
||||
}
|
||||
VStack(spacing: 8) {
|
||||
if match.canResumeCamera {
|
||||
MatchPrimaryButton(label: L10n.t("sheet.resume.camera"), action: onResumeCamera)
|
||||
.accessibilityIdentifier("resume.camera")
|
||||
}
|
||||
if match.activeSessionStatus == "idle" {
|
||||
MatchSecondaryButton(label: L10n.t("sheet.continue.setup"), action: onContinueSetup)
|
||||
.accessibilityIdentifier("resume.continue.setup")
|
||||
}
|
||||
Button(action: onDismiss) {
|
||||
Text(L10n.t("action.cancel"))
|
||||
.font(MatchTypography.labelLarge)
|
||||
.foregroundStyle(MatchColors.textSecondary)
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(height: 44)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityIdentifier("resume.cancel")
|
||||
}
|
||||
.padding(.top, 20)
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.top, 16)
|
||||
.padding(.bottom, 24)
|
||||
.frame(maxWidth: .infinity, alignment: .topLeading)
|
||||
.background(MatchColors.surface)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous))
|
||||
.accessibilityElement(children: .contain)
|
||||
.accessibilityIdentifier("resume.sheet.panel")
|
||||
}
|
||||
}
|
||||
|
||||
private struct ScheduleMatchSheet: View {
|
||||
@ObservedObject var container: AppContainer
|
||||
let teamId: String?
|
||||
let onCreated: (Match) -> Void
|
||||
var onFailed: (Error) -> Void = { _ in }
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@State private var opponent = ""
|
||||
@State private var location = ""
|
||||
@State private var date = Date().addingTimeInterval(86400)
|
||||
@State private var saving = false
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
@@ -593,14 +691,15 @@ private struct ScheduleMatchSheet: View {
|
||||
}
|
||||
ToolbarItem(placement: .confirmationAction) {
|
||||
Button(L10n.t("sheet.save.schedule")) { create() }
|
||||
.disabled(opponent.isEmpty || teamId == nil)
|
||||
.disabled(opponent.isEmpty || teamId == nil || saving)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func create() {
|
||||
guard let teamId else { return }
|
||||
guard let teamId, !saving else { return }
|
||||
saving = true
|
||||
Task {
|
||||
do {
|
||||
let match = try await container.matchRepository.createScheduledMatch(
|
||||
@@ -611,7 +710,8 @@ private struct ScheduleMatchSheet: View {
|
||||
)
|
||||
onCreated(match)
|
||||
} catch {
|
||||
// Sheet chiude solo su successo; errori gestiti dal chiamante via snackbar se necessario.
|
||||
onFailed(error)
|
||||
saving = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import PhotosUI
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
|
||||
struct MatchCoverSection: View {
|
||||
let effectiveCoverUrl: String?
|
||||
@@ -29,6 +30,7 @@ struct MatchCoverSection: View {
|
||||
.font(MatchTypography.bodyMedium)
|
||||
.foregroundStyle(MatchColors.textSecondary)
|
||||
.padding(.top, 4)
|
||||
.accessibilityIdentifier("wizard.cover.source")
|
||||
|
||||
Group {
|
||||
if let localCoverImage {
|
||||
@@ -68,8 +70,20 @@ struct MatchCoverSection: View {
|
||||
.overlay(RoundedRectangle(cornerRadius: 8).stroke(MatchColors.outline, lineWidth: 1))
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityIdentifier("wizard.cover.change")
|
||||
.padding(.top, 12)
|
||||
|
||||
if ProcessInfo.processInfo.arguments.contains("--uitesting-reset") {
|
||||
Button("Usa copertina di test") {
|
||||
localCoverImage = Self.makeUiTestCoverImage()
|
||||
}
|
||||
.font(MatchTypography.labelLarge)
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(height: 44)
|
||||
.accessibilityIdentifier("wizard.cover.uitest")
|
||||
.padding(.top, 8)
|
||||
}
|
||||
|
||||
if hasMatchOverride || localCoverImage != nil {
|
||||
MatchSecondaryButton(
|
||||
label: L10n.t("wizard.match.cover.reset"),
|
||||
@@ -94,4 +108,14 @@ struct MatchCoverSection: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// PNG 16:9 magenta, usata solo dai UI test (`--uitesting-reset`).
|
||||
static func makeUiTestCoverImage() -> UIImage {
|
||||
let size = CGSize(width: 1280, height: 720)
|
||||
let renderer = UIGraphicsImageRenderer(size: size)
|
||||
return renderer.image { ctx in
|
||||
UIColor(red: 1, green: 0.125, blue: 0.75, alpha: 1).setFill()
|
||||
ctx.fill(CGRect(origin: .zero, size: size))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -224,6 +224,7 @@ struct StepMatchScreen: View {
|
||||
}
|
||||
|
||||
MatchPrimaryButton(label: L10n.t("wizard.action.next"), action: saveAndContinue, enabled: !saving, loading: saving)
|
||||
.accessibilityIdentifier("wizard.next")
|
||||
.padding(.top, 32)
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
|
||||
@@ -92,7 +92,8 @@ struct StepNetworkTestScreen: View {
|
||||
if testCompleted, let shareUrl {
|
||||
WizardReadOnlyField(
|
||||
label: currentSession.platform == "youtube" ? L10n.t("wizard.network.link.youtube.label") : L10n.t("wizard.network.link.live.label"),
|
||||
value: shareUrl
|
||||
value: shareUrl,
|
||||
valueIdentifier: "wizard.network.shareurl"
|
||||
)
|
||||
.padding(.top, 16)
|
||||
|
||||
@@ -125,6 +126,7 @@ struct StepNetworkTestScreen: View {
|
||||
action: runTest,
|
||||
enabled: !testing
|
||||
)
|
||||
.accessibilityIdentifier("wizard.network.test")
|
||||
.padding(.top, 24)
|
||||
}
|
||||
|
||||
@@ -135,12 +137,14 @@ struct StepNetworkTestScreen: View {
|
||||
HStack(spacing: spacing) {
|
||||
MatchSecondaryButton(label: L10n.t("wizard.action.back"), action: onBack, enabled: !starting)
|
||||
.frame(width: backWidth)
|
||||
.accessibilityIdentifier("wizard.back")
|
||||
MatchPrimaryButton(
|
||||
label: L10n.t("wizard.action.start"),
|
||||
action: startLive,
|
||||
enabled: ready,
|
||||
loading: starting
|
||||
)
|
||||
.accessibilityIdentifier("wizard.start")
|
||||
.frame(width: forwardWidth)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ struct StepTransmissionScreen: View {
|
||||
|
||||
@State private var team: Team?
|
||||
@State private var platform = "matchlivetv"
|
||||
@State private var privacy = "public"
|
||||
@State private var privacy = ProcessInfo.processInfo.arguments.contains("--uitesting-reset") ? "unlisted" : "public"
|
||||
@State private var creating = false
|
||||
|
||||
private var youtubeReady: Bool {
|
||||
@@ -42,6 +42,7 @@ struct StepTransmissionScreen: View {
|
||||
selected: platform == "matchlivetv",
|
||||
onClick: { platform = "matchlivetv" }
|
||||
)
|
||||
.accessibilityIdentifier("wizard.platform.site")
|
||||
.padding(.top, 12)
|
||||
|
||||
WizardPlatformCard(
|
||||
@@ -70,6 +71,7 @@ struct StepTransmissionScreen: View {
|
||||
selected: privacy == "unlisted",
|
||||
action: { privacy = "unlisted" }
|
||||
)
|
||||
.accessibilityIdentifier("wizard.privacy.unlisted")
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.padding(.top, 12)
|
||||
|
||||
@@ -44,6 +44,7 @@ struct TeamBrandingRow: View {
|
||||
.font(MatchTypography.titleMedium)
|
||||
.padding(8)
|
||||
.overlay(RoundedRectangle(cornerRadius: 8).stroke(MatchColors.outline, lineWidth: 1))
|
||||
.accessibilityIdentifier("wizard.opponent")
|
||||
} else {
|
||||
Text(teamName.isEmpty ? L10n.t("wizard.branding.team.fallback.name") : teamName)
|
||||
.font(MatchTypography.titleMedium)
|
||||
|
||||
@@ -37,9 +37,11 @@ struct WizardFooterButtons: View {
|
||||
if showBack {
|
||||
MatchSecondaryButton(label: L10n.t("wizard.action.back"), action: onBack)
|
||||
.frame(maxWidth: .infinity)
|
||||
.accessibilityIdentifier("wizard.back")
|
||||
}
|
||||
MatchPrimaryButton(label: forwardLabel, action: onForward, loading: forwardLoading)
|
||||
.frame(maxWidth: .infinity)
|
||||
.accessibilityIdentifier("wizard.next")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -66,11 +68,18 @@ struct WizardOutlinedField: View {
|
||||
struct WizardReadOnlyField: View {
|
||||
let label: String
|
||||
let value: String
|
||||
var valueIdentifier: String? = nil
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(label).font(MatchTypography.bodyMedium)
|
||||
Text(value).font(MatchTypography.titleMedium)
|
||||
if let valueIdentifier {
|
||||
Text(value)
|
||||
.font(MatchTypography.titleMedium)
|
||||
.accessibilityIdentifier(valueIdentifier)
|
||||
} else {
|
||||
Text(value).font(MatchTypography.titleMedium)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(14)
|
||||
|
||||
@@ -30,6 +30,7 @@ struct WizardShellScreen: View {
|
||||
Image(systemName: "xmark").foregroundStyle(.white)
|
||||
}
|
||||
.accessibilityLabel(L10n.t("common.close"))
|
||||
.accessibilityIdentifier("wizard.close")
|
||||
Text(wizardStepTitle(currentStep))
|
||||
.font(MatchTypography.titleMedium)
|
||||
Spacer(minLength: 0)
|
||||
|
||||
@@ -0,0 +1,727 @@
|
||||
import XCTest
|
||||
|
||||
/// Pilota il simulatore come un utente (backend da API_BASE_URL).
|
||||
final class HubFlowsUITests: XCTestCase {
|
||||
private var app: XCUIApplication!
|
||||
private let shotDir = "/tmp/mltv-uitest"
|
||||
private var apiBaseUrl = "http://localhost:3000"
|
||||
|
||||
override func setUpWithError() throws {
|
||||
continueAfterFailure = false
|
||||
addUIInterruptionMonitor(withDescription: "Dialog di sistema") { alert in
|
||||
for label in [
|
||||
"Non ora", "Not Now", "Not now",
|
||||
"Consenti", "OK", "Allow", "Allow Once", "Consenti una volta",
|
||||
"Allow While Using App", "Consenti durante l'utilizzo",
|
||||
"Allow While Using the App"
|
||||
] {
|
||||
if alert.buttons[label].exists {
|
||||
alert.buttons[label].tap()
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
try FileManager.default.createDirectory(atPath: shotDir, withIntermediateDirectories: true)
|
||||
app = XCUIApplication()
|
||||
// L'URL API arriva dall'Info.plist dell'app ($(API_BASE_URL) di xcodebuild).
|
||||
// Non iniettare un default localhost: maschererebbe il collaudo.
|
||||
if let fromEnv = ProcessInfo.processInfo.environment["API_BASE_URL"]?.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||
!fromEnv.isEmpty {
|
||||
apiBaseUrl = fromEnv
|
||||
app.launchEnvironment["API_BASE_URL"] = fromEnv
|
||||
}
|
||||
app.launchArguments = ["--uitesting-reset", "-AppleLanguages", "(it)", "-AppleLocale", "it_IT"]
|
||||
app.launch()
|
||||
}
|
||||
|
||||
func testLanguagesOnLogin() throws {
|
||||
waitLogin()
|
||||
dismissKeyboard()
|
||||
capture("lang-00-it-login")
|
||||
XCTAssertTrue(
|
||||
app.staticTexts["ACCEDI"].exists || app.buttons["ACCEDI"].exists,
|
||||
"Login italiano atteso (ACCEDI)"
|
||||
)
|
||||
XCTAssertTrue(any("login.forgot").exists || app.buttons["Password dimenticata?"].exists)
|
||||
|
||||
assertLanguageSwitch(
|
||||
code: "en",
|
||||
loginTitle: "LOG IN",
|
||||
forgot: "Forgot password?",
|
||||
shot: "lang-01-en-login"
|
||||
)
|
||||
assertLanguageSwitch(
|
||||
code: "fr",
|
||||
loginTitle: "CONNEXION",
|
||||
forgot: "Mot de passe oublié ?",
|
||||
shot: "lang-02-fr-login"
|
||||
)
|
||||
assertLanguageSwitch(
|
||||
code: "de",
|
||||
loginTitle: "ANMELDEN",
|
||||
forgot: "Passwort vergessen?",
|
||||
shot: "lang-03-de-login"
|
||||
)
|
||||
assertLanguageSwitch(
|
||||
code: "es",
|
||||
loginTitle: "ACCEDER",
|
||||
forgot: "¿Olvidaste la contraseña?",
|
||||
shot: "lang-04-es-login"
|
||||
)
|
||||
assertLanguageSwitch(
|
||||
code: "it",
|
||||
loginTitle: "ACCEDI",
|
||||
forgot: "Password dimenticata?",
|
||||
shot: "lang-05-it-login"
|
||||
)
|
||||
}
|
||||
|
||||
func testCoachFlows() throws {
|
||||
waitLogin()
|
||||
dismissKeyboard()
|
||||
capture("01-login")
|
||||
|
||||
let forgot = firstExisting(["login.forgot"], labels: ["Password dimenticata?", "Forgot password?"])
|
||||
XCTAssertTrue(forgot.waitForExistence(timeout: 8), "Link password dimenticata assente")
|
||||
tapHittable(forgot)
|
||||
XCTAssertTrue(any("forgot.back").waitForExistence(timeout: 8), "Schermata password dimenticata assente")
|
||||
capture("02-forgot-password")
|
||||
|
||||
let mail = app.textFields.firstMatch
|
||||
XCTAssertTrue(mail.waitForExistence(timeout: 4))
|
||||
tapHittable(mail)
|
||||
mail.typeText("coach@matchlivetv.test")
|
||||
dismissKeyboard()
|
||||
tapHittable(firstExisting(["forgot.submit"], labels: ["Invia link di reset", "Send reset link"]))
|
||||
XCTAssertTrue(
|
||||
app.staticTexts.matching(NSPredicate(format: "label CONTAINS 'link'")).firstMatch.waitForExistence(timeout: 12)
|
||||
|| app.staticTexts.matching(NSPredicate(format: "label CONTAINS 'registrat'")).firstMatch.exists,
|
||||
"Manca il messaggio di esito del reset password.\n\(app.debugDescription)"
|
||||
)
|
||||
capture("02b-forgot-submitted")
|
||||
tapHittable(app.buttons["forgot.back"])
|
||||
|
||||
loginIfNeeded()
|
||||
waitHub()
|
||||
dismissSystemDialogs()
|
||||
capture("03-hub-dopo-login")
|
||||
|
||||
openLanguagePicker(from: "matches.language")
|
||||
tapHittable(any("language.en"))
|
||||
XCTAssertTrue(
|
||||
app.buttons["NEW MATCH"].waitForExistence(timeout: 8) || any("matches.new").waitForExistence(timeout: 4),
|
||||
"Hub non passato in inglese"
|
||||
)
|
||||
capture("03b-hub-en")
|
||||
openLanguagePicker(from: "matches.language")
|
||||
tapHittable(any("language.it"))
|
||||
XCTAssertTrue(
|
||||
app.buttons["NUOVA PARTITA"].waitForExistence(timeout: 8) || any("matches.new").waitForExistence(timeout: 4),
|
||||
"Hub non tornato in italiano"
|
||||
)
|
||||
|
||||
openAccountAndBack()
|
||||
capture("04-hub-dopo-account")
|
||||
|
||||
scheduleMatchShowsConfigureDialog()
|
||||
capture("05-dopo-programmazione")
|
||||
}
|
||||
|
||||
func testLogoutReturnsToLogin() throws {
|
||||
loginIfNeeded()
|
||||
waitHub()
|
||||
dismissSystemDialogs()
|
||||
tapHittable(firstExisting(["matches.account"], labels: ["Account"]))
|
||||
XCTAssertTrue(any("account.back").waitForExistence(timeout: 8), "Account non aperto")
|
||||
app.swipeUp()
|
||||
app.swipeUp()
|
||||
XCTAssertTrue(any("account.logout").waitForExistence(timeout: 6), "Bottone Esci assente")
|
||||
tapHittable(any("account.logout"))
|
||||
XCTAssertTrue(
|
||||
any("login.submit").waitForExistence(timeout: 12) || app.staticTexts["ACCEDI"].waitForExistence(timeout: 4),
|
||||
"Logout non ha riportato al login"
|
||||
)
|
||||
capture("14-dopo-logout")
|
||||
}
|
||||
|
||||
func testWizardNetworkAndLive() throws {
|
||||
loginIfNeeded()
|
||||
waitHub()
|
||||
dismissSystemDialogs()
|
||||
tapNewMatch()
|
||||
let quick = firstExisting(["sheet.quick.option"], labels: ["Avvia subito"])
|
||||
XCTAssertTrue(quick.waitForExistence(timeout: 8))
|
||||
tapHittable(quick)
|
||||
XCTAssertTrue(
|
||||
app.staticTexts["01 · Partita"].waitForExistence(timeout: 25),
|
||||
"Wizard step 1 assente"
|
||||
)
|
||||
capture("06-wizard-step1")
|
||||
|
||||
let opponent = any("wizard.opponent").exists ? any("wizard.opponent") : app.textFields.firstMatch
|
||||
if opponent.waitForExistence(timeout: 8) {
|
||||
tapHittable(opponent)
|
||||
if let current = opponent.value as? String, !current.isEmpty {
|
||||
let deletes = String(repeating: XCUIKeyboardKey.delete.rawValue, count: current.count)
|
||||
opponent.typeText(deletes)
|
||||
}
|
||||
opponent.typeText("UI Wizard")
|
||||
dismissKeyboard()
|
||||
}
|
||||
applyMatchCover()
|
||||
scrollToId("wizard.next")
|
||||
tapHittable(any("wizard.next"))
|
||||
XCTAssertTrue(
|
||||
app.staticTexts["02 · Trasmissione"].waitForExistence(timeout: 20)
|
||||
|| app.staticTexts.matching(NSPredicate(format: "label CONTAINS 'Trasmissione' OR label CONTAINS 'Transmission'")).firstMatch.waitForExistence(timeout: 8),
|
||||
"Wizard step 2 (trasmissione) non aperto.\n\(app.debugDescription)"
|
||||
)
|
||||
capture("07-wizard-step2")
|
||||
if any("wizard.platform.site").waitForExistence(timeout: 4) {
|
||||
tapHittable(any("wizard.platform.site"))
|
||||
}
|
||||
let unlisted = firstExisting(["wizard.privacy.unlisted"], labels: ["NON IN ELENCO", "UNLISTED"])
|
||||
XCTAssertTrue(unlisted.waitForExistence(timeout: 6), "Manca l'opzione NON IN ELENCO")
|
||||
tapHittable(unlisted)
|
||||
XCTAssertTrue(
|
||||
app.staticTexts.matching(NSPredicate(format: "label CONTAINS 'Solo chi ha il link' OR label CONTAINS 'Only those with the link'")).firstMatch.waitForExistence(timeout: 4)
|
||||
|| any("wizard.privacy.unlisted").exists,
|
||||
"Visibilità non impostata su privato/unlisted"
|
||||
)
|
||||
capture("07b-privacy-unlisted")
|
||||
scrollToId("wizard.next")
|
||||
tapHittable(any("wizard.next"))
|
||||
XCTAssertTrue(
|
||||
app.staticTexts["03 · Test rete"].waitForExistence(timeout: 20)
|
||||
|| any("wizard.network.test").waitForExistence(timeout: 10),
|
||||
"Wizard step 3 (rete) non aperto.\n\(app.debugDescription)"
|
||||
)
|
||||
capture("08-wizard-step3")
|
||||
|
||||
scrollToId("wizard.network.test")
|
||||
tapHittable(any("wizard.network.test"))
|
||||
XCTAssertTrue(
|
||||
app.staticTexts["PRONTO PER ANDARE IN DIRETTA"].waitForExistence(timeout: 20)
|
||||
|| app.staticTexts.matching(NSPredicate(format: "label CONTAINS 'PRONTO' OR label CONTAINS 'READY'")).firstMatch.waitForExistence(timeout: 10),
|
||||
"Test rete non completato.\n\(app.debugDescription)"
|
||||
)
|
||||
capture("08b-network-ready")
|
||||
let tree = app.debugDescription
|
||||
XCTAssertFalse(tree.contains("youtube.com"), "Il test ha creato un video YouTube: deve restare sul sito, unlisted")
|
||||
XCTAssertTrue(
|
||||
tree.contains("matchlivetv.it/live") || tree.contains("/live/"),
|
||||
"Manca il link diretta sul sito"
|
||||
)
|
||||
let sessionId = extractLiveSessionId()
|
||||
XCTAssertNotNil(sessionId, "Impossibile leggere l'id sessione dal link diretta.\n\(tree)")
|
||||
if let sessionId {
|
||||
try? sessionId.write(toFile: "\(shotDir)/live-session-id.txt", atomically: true, encoding: .utf8)
|
||||
assertLivePageIsUnlistedWithMatchCover(sessionId: sessionId)
|
||||
}
|
||||
|
||||
let start = any("wizard.start")
|
||||
XCTAssertTrue(start.waitForExistence(timeout: 6))
|
||||
tapHittable(start)
|
||||
dismissSystemDialogs()
|
||||
let liveVisible =
|
||||
any("broadcast.terminate").waitForExistence(timeout: 25)
|
||||
|| any("broadcast.pause").waitForExistence(timeout: 4)
|
||||
|| app.buttons["CONCEDI PERMESSI"].waitForExistence(timeout: 4)
|
||||
|| app.staticTexts.matching(NSPredicate(format: "label CONTAINS 'camera' OR label CONTAINS 'microfono' OR label CONTAINS 'Permessi'")).firstMatch.waitForExistence(timeout: 4)
|
||||
capture("09-broadcast")
|
||||
XCTAssertTrue(liveVisible, "Dopo INIZIA non si apre la diretta né la richiesta permessi.\n\(app.debugDescription)")
|
||||
|
||||
if any("broadcast.mute").waitForExistence(timeout: 3) {
|
||||
tapHittable(any("broadcast.mute"))
|
||||
sleep(1)
|
||||
capture("09b-mute-confirm")
|
||||
if app.buttons["Conferma"].waitForExistence(timeout: 2) || app.alerts.buttons.element(boundBy: 1).exists {
|
||||
let confirm = app.buttons["Conferma"].exists ? app.buttons["Conferma"] : app.alerts.buttons.element(boundBy: 1)
|
||||
tapHittable(confirm)
|
||||
}
|
||||
capture("09c-muted")
|
||||
}
|
||||
|
||||
if any("broadcast.score.home.plus").waitForExistence(timeout: 3) {
|
||||
tapHittable(any("broadcast.score.home.plus"))
|
||||
sleep(1)
|
||||
capture("09c2-score-plus")
|
||||
}
|
||||
if any("broadcast.abr").waitForExistence(timeout: 2) {
|
||||
tapHittable(any("broadcast.abr"))
|
||||
sleep(1)
|
||||
capture("09c3-abr")
|
||||
if app.buttons["Annulla"].waitForExistence(timeout: 2) {
|
||||
tapHittable(app.buttons["Annulla"])
|
||||
}
|
||||
}
|
||||
if any("broadcast.share").waitForExistence(timeout: 2) {
|
||||
tapHittable(any("broadcast.share"))
|
||||
sleep(1)
|
||||
if app.buttons["Conferma"].waitForExistence(timeout: 3) {
|
||||
tapHittable(app.buttons["Conferma"])
|
||||
}
|
||||
sleep(1)
|
||||
capture("09c4-share")
|
||||
let spring = XCUIApplication(bundleIdentifier: "com.apple.springboard")
|
||||
for label in ["Chiudi", "Close", "Annulla", "Cancel"] {
|
||||
if spring.buttons[label].exists {
|
||||
spring.buttons[label].tap()
|
||||
break
|
||||
}
|
||||
if app.buttons[label].exists {
|
||||
tapHittable(app.buttons[label])
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if any("broadcast.pause").waitForExistence(timeout: 4) {
|
||||
tapHittable(any("broadcast.pause"))
|
||||
sleep(1)
|
||||
capture("09c5-pause-confirm")
|
||||
let confirmPause = firstExisting([], labels: ["Conferma", "Confirm"])
|
||||
if confirmPause.waitForExistence(timeout: 3) {
|
||||
tapHittable(confirmPause)
|
||||
}
|
||||
sleep(3)
|
||||
capture("09c6-paused")
|
||||
XCTAssertTrue(
|
||||
app.staticTexts["PAUSA"].waitForExistence(timeout: 8)
|
||||
|| app.staticTexts.matching(NSPredicate(format: "label CONTAINS 'PAUSA' OR label CONTAINS 'PAUSED'")).firstMatch.exists,
|
||||
"Dopo la conferma non compare lo stato PAUSA"
|
||||
)
|
||||
if let sessionId {
|
||||
assertViewerShowsCoverWhilePaused(sessionId: sessionId)
|
||||
}
|
||||
}
|
||||
|
||||
if any("broadcast.terminate").waitForExistence(timeout: 3) {
|
||||
tapHittable(any("broadcast.terminate"))
|
||||
sleep(1)
|
||||
capture("09d-terminate-confirm")
|
||||
for label in ["TERMINA", "END", "BEENDEN", "TERMINAR", "TERMINER"] {
|
||||
if app.buttons[label].exists {
|
||||
tapHittable(app.buttons[label])
|
||||
break
|
||||
}
|
||||
}
|
||||
} else if any("wizard.close").waitForExistence(timeout: 2) {
|
||||
tapHittable(any("wizard.close"))
|
||||
}
|
||||
_ = any("matches.new").waitForExistence(timeout: 12)
|
||||
capture("09e-back-hub")
|
||||
}
|
||||
|
||||
private func applyMatchCover() {
|
||||
scrollToId("wizard.cover.change")
|
||||
let change = firstExisting(["wizard.cover.change"], labels: ["Cambia copertina per questa partita"])
|
||||
XCTAssertTrue(change.waitForExistence(timeout: 8), "Manca Cambia copertina per questa partita")
|
||||
|
||||
scrollToId("wizard.cover.uitest")
|
||||
let testCover = any("wizard.cover.uitest")
|
||||
XCTAssertTrue(testCover.waitForExistence(timeout: 6), "Manca il fallback copertina di test")
|
||||
tapHittable(testCover)
|
||||
XCTAssertTrue(
|
||||
app.staticTexts.matching(
|
||||
NSPredicate(format: "label CONTAINS 'Questa partita' OR label CONTAINS 'This match'")
|
||||
).firstMatch.waitForExistence(timeout: 8),
|
||||
"La copertina di test non è stata applicata a questa partita.\n\(app.debugDescription)"
|
||||
)
|
||||
capture("06c-cover-match")
|
||||
|
||||
tapHittable(change)
|
||||
sleep(2)
|
||||
capture("06b-cover-picker")
|
||||
_ = pickFirstPhotoFromSystemPicker()
|
||||
dismissPhotoPicker()
|
||||
XCTAssertTrue(
|
||||
app.staticTexts.matching(
|
||||
NSPredicate(format: "label CONTAINS 'Questa partita' OR label CONTAINS 'This match'")
|
||||
).firstMatch.waitForExistence(timeout: 6),
|
||||
"Dopo il selettore foto la copertina della partita è sparita"
|
||||
)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
private func pickFirstPhotoFromSystemPicker() -> Bool {
|
||||
let photos = XCUIApplication(bundleIdentifier: "com.apple.PhotosUIService")
|
||||
guard photos.wait(for: .runningForeground, timeout: 5) else { return false }
|
||||
let candidates: [XCUIElement] = [
|
||||
photos.images.firstMatch,
|
||||
photos.collectionViews.cells.firstMatch,
|
||||
photos.cells.firstMatch
|
||||
]
|
||||
for el in candidates {
|
||||
if el.waitForExistence(timeout: 2) {
|
||||
el.tap()
|
||||
sleep(1)
|
||||
for label in ["Aggiungi", "Add", "Scegli", "Choose", "Done", "Fine"] {
|
||||
if photos.buttons[label].exists {
|
||||
photos.buttons[label].tap()
|
||||
break
|
||||
}
|
||||
}
|
||||
let source = app.staticTexts.matching(
|
||||
NSPredicate(format: "label CONTAINS 'Questa partita' OR label CONTAINS 'This match'")
|
||||
).firstMatch
|
||||
if source.waitForExistence(timeout: 4) { return true }
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private func dismissPhotoPicker() {
|
||||
let photos = XCUIApplication(bundleIdentifier: "com.apple.PhotosUIService")
|
||||
let spring = XCUIApplication(bundleIdentifier: "com.apple.springboard")
|
||||
for label in ["Annulla", "Cancel", "Chiudi", "Close"] {
|
||||
if photos.buttons[label].exists {
|
||||
photos.buttons[label].tap()
|
||||
sleep(1)
|
||||
return
|
||||
}
|
||||
if spring.buttons[label].exists {
|
||||
spring.buttons[label].tap()
|
||||
sleep(1)
|
||||
return
|
||||
}
|
||||
if app.buttons[label].exists {
|
||||
tapHittable(app.buttons[label])
|
||||
sleep(1)
|
||||
return
|
||||
}
|
||||
}
|
||||
if photos.state == .runningForeground {
|
||||
photos.swipeDown()
|
||||
sleep(1)
|
||||
}
|
||||
}
|
||||
|
||||
private func extractLiveSessionId() -> String? {
|
||||
let share = any("wizard.network.shareurl")
|
||||
let raw = share.exists ? share.label : app.debugDescription
|
||||
guard let range = raw.range(of: #"https?://[^\s]+/live/[0-9a-fA-F-]{36}"#, options: .regularExpression) else {
|
||||
return nil
|
||||
}
|
||||
let url = String(raw[range])
|
||||
return url.components(separatedBy: "/live/").last
|
||||
}
|
||||
|
||||
private func assertLivePageIsUnlistedWithMatchCover(sessionId: String) {
|
||||
let pageURL = "https://www.matchlivetv.it/live/\(sessionId)"
|
||||
let deadline = Date().addingTimeInterval(40)
|
||||
var html = ""
|
||||
var poster = ""
|
||||
while Date() < deadline {
|
||||
html = httpGet(pageURL) ?? ""
|
||||
if html.contains("youtube.com/watch") {
|
||||
XCTFail("La pagina pubblica punta a YouTube invece che al sito")
|
||||
return
|
||||
}
|
||||
if let range = html.range(of: #"poster="([^"]+)""#, options: .regularExpression) {
|
||||
poster = String(html[range])
|
||||
.replacingOccurrences(of: "poster=\"", with: "")
|
||||
.replacingOccurrences(of: "\"", with: "")
|
||||
}
|
||||
let unlisted = html.contains("noindex")
|
||||
let customPoster = !poster.isEmpty && !poster.contains("copertina-canale")
|
||||
if unlisted && customPoster { break }
|
||||
RunLoop.current.run(until: Date().addingTimeInterval(2))
|
||||
}
|
||||
XCTAssertTrue(html.contains("noindex") || html.contains("nofollow"), "La diretta deve restare non in elenco (noindex)")
|
||||
XCTAssertFalse(poster.isEmpty, "Manca l'attributo poster sul player live")
|
||||
XCTAssertFalse(poster.contains("copertina-canale"), "Il poster è ancora la copertina di default: \(poster)")
|
||||
try? html.write(toFile: "\(shotDir)/live-page.html", atomically: true, encoding: .utf8)
|
||||
try? poster.write(toFile: "\(shotDir)/live-poster.txt", atomically: true, encoding: .utf8)
|
||||
}
|
||||
|
||||
private func assertViewerShowsCoverWhilePaused(sessionId: String) {
|
||||
let statusURL = "https://www.matchlivetv.it/live/\(sessionId)/status.json"
|
||||
let deadline = Date().addingTimeInterval(25)
|
||||
var payload: [String: Any] = [:]
|
||||
while Date() < deadline {
|
||||
payload = httpGetJSON(statusURL) ?? [:]
|
||||
let paused = payload["paused"] as? Bool ?? false
|
||||
let status = payload["status"] as? String ?? ""
|
||||
let showingCover = payload["showing_cover"] as? Bool ?? false
|
||||
if (paused || status == "paused") && showingCover {
|
||||
break
|
||||
}
|
||||
RunLoop.current.run(until: Date().addingTimeInterval(1.5))
|
||||
}
|
||||
if let data = try? JSONSerialization.data(withJSONObject: payload, options: [.prettyPrinted]),
|
||||
let text = String(data: data, encoding: .utf8) {
|
||||
try? text.write(toFile: "\(shotDir)/live-status-paused.json", atomically: true, encoding: .utf8)
|
||||
}
|
||||
let paused = payload["paused"] as? Bool ?? false
|
||||
let status = payload["status"] as? String ?? ""
|
||||
let showingCover = payload["showing_cover"] as? Bool ?? false
|
||||
XCTAssertTrue(paused || status == "paused", "status.json non è in pausa: \(payload)")
|
||||
XCTAssertTrue(showingCover, "Gli spettatori non stanno vedendo la copertina (showing_cover=false): \(payload)")
|
||||
}
|
||||
|
||||
private func httpGet(_ urlString: String) -> String? {
|
||||
guard let url = URL(string: urlString) else { return nil }
|
||||
let sem = DispatchSemaphore(value: 0)
|
||||
var result: String?
|
||||
URLSession.shared.dataTask(with: url) { data, _, _ in
|
||||
if let data { result = String(data: data, encoding: .utf8) }
|
||||
sem.signal()
|
||||
}.resume()
|
||||
_ = sem.wait(timeout: .now() + 15)
|
||||
return result
|
||||
}
|
||||
|
||||
private func httpGetJSON(_ urlString: String) -> [String: Any]? {
|
||||
guard let body = httpGet(urlString),
|
||||
let data = body.data(using: .utf8),
|
||||
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
|
||||
return nil
|
||||
}
|
||||
return json
|
||||
}
|
||||
|
||||
func testResumeLiveSheet() throws {
|
||||
loginIfNeeded()
|
||||
waitHub()
|
||||
dismissSystemDialogs()
|
||||
capture("10-hub-con-banner")
|
||||
|
||||
let banner = app.buttons["matches.resume.banner"]
|
||||
if !banner.waitForExistence(timeout: 10) {
|
||||
throw XCTSkip("Nessuna sessione attiva da riprendere su \(apiBaseUrl)")
|
||||
}
|
||||
tapHittable(banner)
|
||||
sleep(1)
|
||||
capture("11-dopo-tap-banner")
|
||||
|
||||
let sheetVisible =
|
||||
any("resume.sheet").waitForExistence(timeout: 4)
|
||||
|| any("resume.sheet.panel").waitForExistence(timeout: 2)
|
||||
|| any("resume.camera").waitForExistence(timeout: 2)
|
||||
|| app.staticTexts["Diretta in corso"].waitForExistence(timeout: 2)
|
||||
XCTAssertTrue(sheetVisible, "Lo sheet di ripresa non è comparso.\n\(app.debugDescription)")
|
||||
capture("12-resume-sheet")
|
||||
|
||||
let cancel = firstExisting(["resume.cancel"], labels: ["Annulla", "Cancel"])
|
||||
if cancel.waitForExistence(timeout: 3) {
|
||||
tapHittable(cancel)
|
||||
}
|
||||
}
|
||||
|
||||
private func assertLanguageSwitch(code: String, loginTitle: String, forgot: String, shot: String) {
|
||||
openLanguagePicker(from: "login.language")
|
||||
let row = any("language.\(code)")
|
||||
XCTAssertTrue(row.waitForExistence(timeout: 6), "Voce lingua \(code) assente")
|
||||
tapHittable(row)
|
||||
XCTAssertTrue(
|
||||
app.staticTexts[loginTitle].waitForExistence(timeout: 8) || app.buttons[loginTitle].waitForExistence(timeout: 2),
|
||||
"Dopo lingua \(code) manca il titolo \(loginTitle).\n\(app.debugDescription)"
|
||||
)
|
||||
XCTAssertTrue(
|
||||
any("login.forgot").waitForExistence(timeout: 4)
|
||||
|| app.buttons[forgot].waitForExistence(timeout: 2)
|
||||
|| app.staticTexts[forgot].waitForExistence(timeout: 1),
|
||||
"Dopo lingua \(code) manca '\(forgot)'"
|
||||
)
|
||||
capture(shot)
|
||||
}
|
||||
|
||||
private func openLanguagePicker(from id: String) {
|
||||
let globe = any(id)
|
||||
XCTAssertTrue(globe.waitForExistence(timeout: 8), "Bottone lingua \(id) assente")
|
||||
tapHittable(globe)
|
||||
XCTAssertTrue(
|
||||
any("language.it").waitForExistence(timeout: 8) || app.staticTexts["Italiano"].waitForExistence(timeout: 4),
|
||||
"Picker lingua non aperto"
|
||||
)
|
||||
}
|
||||
|
||||
private func loginIfNeeded() {
|
||||
if hubVisible() { return }
|
||||
waitLogin()
|
||||
dismissKeyboard()
|
||||
let email = app.textFields.firstMatch
|
||||
XCTAssertTrue(email.waitForExistence(timeout: 8))
|
||||
tapHittable(email)
|
||||
email.typeText("coach@matchlivetv.test")
|
||||
let password = app.secureTextFields.firstMatch
|
||||
XCTAssertTrue(password.waitForExistence(timeout: 4))
|
||||
tapHittable(password)
|
||||
password.typeText("Password123")
|
||||
dismissKeyboard()
|
||||
let submit = firstExisting(["login.submit"], labels: ["ACCEDI", "LOG IN", "CONNEXION", "ANMELDEN", "ACCEDER"])
|
||||
XCTAssertTrue(submit.waitForExistence(timeout: 4))
|
||||
tapHittable(submit)
|
||||
dismissSystemDialogs()
|
||||
}
|
||||
|
||||
private func waitLogin() {
|
||||
let deadline = Date().addingTimeInterval(20)
|
||||
while Date() < deadline {
|
||||
if any("login.submit").exists || app.textFields.firstMatch.exists {
|
||||
return
|
||||
}
|
||||
if hubVisible() { return }
|
||||
RunLoop.current.run(until: Date().addingTimeInterval(0.3))
|
||||
}
|
||||
capture("fail-login")
|
||||
XCTFail("Login non comparso. Gerarchia:\n\(app.debugDescription)")
|
||||
}
|
||||
|
||||
private func waitHub() {
|
||||
XCTAssertTrue(
|
||||
any("matches.new").waitForExistence(timeout: 25)
|
||||
|| app.buttons["NUOVA PARTITA"].waitForExistence(timeout: 5)
|
||||
|| app.buttons["NEW MATCH"].waitForExistence(timeout: 2),
|
||||
"Hub partite non raggiunto dopo login su \(apiBaseUrl).\n\(app.debugDescription)"
|
||||
)
|
||||
dismissKeyboard()
|
||||
}
|
||||
|
||||
private func hubVisible() -> Bool {
|
||||
app.buttons["matches.new"].exists || app.buttons["NUOVA PARTITA"].exists || app.buttons["NEW MATCH"].exists
|
||||
}
|
||||
|
||||
private func openAccountAndBack() {
|
||||
let account = firstExisting(["matches.account"], labels: ["Account"])
|
||||
XCTAssertTrue(account.waitForExistence(timeout: 6), "Bottone Account assente")
|
||||
tapHittable(account)
|
||||
XCTAssertTrue(
|
||||
any("account.title").waitForExistence(timeout: 8) || any("account.back").waitForExistence(timeout: 2),
|
||||
"Schermata Account non aperta.\n\(app.debugDescription)"
|
||||
)
|
||||
capture("04a-account")
|
||||
XCTAssertTrue(any("account.save.profile").waitForExistence(timeout: 4), "Manca Salva profilo")
|
||||
tapHittable(any("account.save.profile"))
|
||||
sleep(2)
|
||||
capture("04b-account-saved")
|
||||
tapHittable(app.buttons["account.back"])
|
||||
XCTAssertTrue(any("matches.new").waitForExistence(timeout: 8))
|
||||
}
|
||||
|
||||
private func scheduleMatchShowsConfigureDialog() {
|
||||
tapNewMatch()
|
||||
capture("05a-nuova-partita")
|
||||
let schedule = firstExisting(["sheet.schedule.option"], labels: ["Programma partita"])
|
||||
XCTAssertTrue(schedule.waitForExistence(timeout: 8), "Opzione Programma partita assente")
|
||||
tapHittable(schedule)
|
||||
let opponent = app.textFields["Avversario"].exists ? app.textFields["Avversario"] : app.textFields.firstMatch
|
||||
XCTAssertTrue(opponent.waitForExistence(timeout: 8), "Campo avversario assente")
|
||||
tapHittable(opponent)
|
||||
opponent.typeText("UI Test Programmata")
|
||||
dismissKeyboard()
|
||||
let save = app.buttons["SALVA IN PROGRAMMA"]
|
||||
XCTAssertTrue(save.waitForExistence(timeout: 4))
|
||||
tapHittable(save)
|
||||
XCTAssertTrue(
|
||||
app.staticTexts["Configurare ora?"].waitForExistence(timeout: 12) || app.alerts.firstMatch.waitForExistence(timeout: 4),
|
||||
"Manca il dialog Configura/Più tardi"
|
||||
)
|
||||
capture("05b-dialog-configura")
|
||||
let later = app.buttons["Più tardi"].exists ? app.buttons["Più tardi"] : app.alerts.buttons.element(boundBy: 0)
|
||||
tapHittable(later)
|
||||
XCTAssertTrue(any("matches.new").waitForExistence(timeout: 8))
|
||||
XCTAssertTrue(
|
||||
app.staticTexts["UI Test Programmata"].waitForExistence(timeout: 8)
|
||||
|| app.staticTexts.matching(NSPredicate(format: "label CONTAINS 'Programmata'")).firstMatch.exists,
|
||||
"La partita programmata non compare in hub"
|
||||
)
|
||||
}
|
||||
|
||||
private func tapNewMatch() {
|
||||
let neu = firstExisting(["matches.new"], labels: ["NUOVA PARTITA", "NEW MATCH"])
|
||||
XCTAssertTrue(neu.waitForExistence(timeout: 8))
|
||||
tapHittable(neu)
|
||||
}
|
||||
|
||||
private func scrollToId(_ id: String) {
|
||||
let el = any(id)
|
||||
for _ in 0..<8 {
|
||||
if el.exists && el.isHittable { return }
|
||||
app.swipeUp()
|
||||
}
|
||||
}
|
||||
|
||||
private func any(_ id: String) -> XCUIElement {
|
||||
app.descendants(matching: .any)[id]
|
||||
}
|
||||
|
||||
private func firstExisting(_ ids: [String], labels: [String] = []) -> XCUIElement {
|
||||
for id in ids {
|
||||
let el = any(id)
|
||||
if el.exists { return el }
|
||||
}
|
||||
for label in labels {
|
||||
let button = app.buttons[label]
|
||||
if button.exists { return button }
|
||||
let text = app.staticTexts[label]
|
||||
if text.exists { return text }
|
||||
}
|
||||
if let firstId = ids.first {
|
||||
return any(firstId)
|
||||
}
|
||||
return app.buttons[labels.first ?? ""]
|
||||
}
|
||||
|
||||
private func tapHittable(_ element: XCUIElement) {
|
||||
if element.isHittable {
|
||||
element.tap()
|
||||
return
|
||||
}
|
||||
element.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).tap()
|
||||
}
|
||||
|
||||
private func dismissSystemDialogs() {
|
||||
let spring = XCUIApplication(bundleIdentifier: "com.apple.springboard")
|
||||
_ = spring.buttons["Non ora"].waitForExistence(timeout: 3)
|
||||
|| spring.buttons["Not Now"].waitForExistence(timeout: 1)
|
||||
|| app.alerts.buttons["Non ora"].waitForExistence(timeout: 1)
|
||||
let labels = ["Non ora", "Not Now", "Not now", "OK", "Consenti", "Allow"]
|
||||
for _ in 0..<8 {
|
||||
var tapped = false
|
||||
for label in labels {
|
||||
if spring.buttons[label].exists {
|
||||
spring.buttons[label].tap()
|
||||
tapped = true
|
||||
break
|
||||
}
|
||||
if app.alerts.buttons[label].exists {
|
||||
app.alerts.buttons[label].tap()
|
||||
tapped = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !tapped { break }
|
||||
RunLoop.current.run(until: Date().addingTimeInterval(0.4))
|
||||
}
|
||||
app.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.08)).tap()
|
||||
}
|
||||
|
||||
private func dismissKeyboard() {
|
||||
if !app.keyboards.firstMatch.exists { return }
|
||||
for name in ["Fine", "Done", "Return", "Invio"] {
|
||||
let key = app.keyboards.buttons[name]
|
||||
if key.exists {
|
||||
key.tap()
|
||||
return
|
||||
}
|
||||
}
|
||||
app.swipeDown()
|
||||
sleep(1)
|
||||
if app.keyboards.firstMatch.exists {
|
||||
app.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.12)).tap()
|
||||
}
|
||||
}
|
||||
|
||||
private func capture(_ name: String) {
|
||||
let shot = app.screenshot()
|
||||
try? shot.pngRepresentation.write(to: URL(fileURLWithPath: "\(shotDir)/\(name).png"))
|
||||
let attachment = XCTAttachment(screenshot: shot)
|
||||
attachment.name = name
|
||||
attachment.lifetime = .keepAlways
|
||||
add(attachment)
|
||||
try? app.debugDescription.write(toFile: "\(shotDir)/\(name).txt", atomically: true, encoding: .utf8)
|
||||
}
|
||||
}
|
||||
@@ -96,7 +96,7 @@ lines += [
|
||||
app_settings = """
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 32;
|
||||
CURRENT_PROJECT_VERSION = 35;
|
||||
GENERATE_INFOPLIST_FILE = NO;
|
||||
INFOPLIST_FILE = MatchLiveTv/Resources/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||
@@ -107,7 +107,7 @@ app_settings = """
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.0.10;
|
||||
MARKETING_VERSION = 2.0.12;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.matchlivetv.match-live-tv;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
API_BASE_URL = "https://www.matchlivetv.it";
|
||||
@@ -122,10 +122,10 @@ app_debug_settings = app_settings + """
|
||||
test_settings = """
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 32;
|
||||
CURRENT_PROJECT_VERSION = 35;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||
MARKETING_VERSION = 2.0.10;
|
||||
MARKETING_VERSION = 2.0.12;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.matchlivetv.match-live-tv.tests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
|
||||
Reference in New Issue
Block a user