Compare commits
3
Commits
79850dfc2c
...
1185d0ce61
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1185d0ce61 | ||
|
|
90d481a2d1 | ||
|
|
50b8a8c8e2 |
@@ -7,6 +7,7 @@
|
|||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* 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 */; };
|
06FE9996B295456D8B8C7E96 /* BroadcastPermissions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BFB2F18615747B1A1C9E9B2 /* BroadcastPermissions.swift */; };
|
||||||
095654DAAF6C4EA4B73BAB61 /* LiveScoreDialogHostTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4F8BF2514BB4F5185827B95 /* LiveScoreDialogHostTests.swift */; };
|
095654DAAF6C4EA4B73BAB61 /* LiveScoreDialogHostTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4F8BF2514BB4F5185827B95 /* LiveScoreDialogHostTests.swift */; };
|
||||||
0CB8933AC2B34D4099BBA881 /* MatchStatusBadge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34BB2251A67E401A9F1E789E /* MatchStatusBadge.swift */; };
|
0CB8933AC2B34D4099BBA881 /* MatchStatusBadge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34BB2251A67E401A9F1E789E /* MatchStatusBadge.swift */; };
|
||||||
@@ -107,9 +108,18 @@
|
|||||||
remoteGlobalIDString = F54F6C97361C4E8A96AEDD11;
|
remoteGlobalIDString = F54F6C97361C4E8A96AEDD11;
|
||||||
remoteInfo = MatchLiveTv;
|
remoteInfo = MatchLiveTv;
|
||||||
};
|
};
|
||||||
|
4B3A29180796A5B4C3D2E1F0 /* PBXContainerItemProxy */ = {
|
||||||
|
isa = PBXContainerItemProxy;
|
||||||
|
containerPortal = AE0076753A1840A19E9F565F /* Project object */;
|
||||||
|
proxyType = 1;
|
||||||
|
remoteGlobalIDString = F54F6C97361C4E8A96AEDD11;
|
||||||
|
remoteInfo = MatchLiveTv;
|
||||||
|
};
|
||||||
/* End PBXContainerItemProxy section */
|
/* End PBXContainerItemProxy section */
|
||||||
|
|
||||||
/* Begin PBXFileReference 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>"; };
|
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>"; };
|
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>"; };
|
10AD0F18190542818572CAEF /* ScoreActionDecodeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ScoreActionDecodeTests.swift; path = MatchLiveTvTests/ScoreActionDecodeTests.swift; sourceTree = "<group>"; };
|
||||||
@@ -228,6 +238,7 @@
|
|||||||
children = (
|
children = (
|
||||||
CE0F9D7D5EE749E8AAE6AB43 /* MatchLiveTv.app */,
|
CE0F9D7D5EE749E8AAE6AB43 /* MatchLiveTv.app */,
|
||||||
3FC0CC8155BE469ABB420DC5 /* MatchLiveTvTests.xctest */,
|
3FC0CC8155BE469ABB420DC5 /* MatchLiveTvTests.xctest */,
|
||||||
|
7E6D5C4B3A29180796A5B4C3 /* MatchLiveTvUITests.xctest */,
|
||||||
);
|
);
|
||||||
name = Products;
|
name = Products;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -248,11 +259,20 @@
|
|||||||
name = MatchLiveTvTests;
|
name = MatchLiveTvTests;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
2A180796A5B4C3D2E1F0A98B /* MatchLiveTvUITests */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
9C8B7A6D5E4F3A2B1C0D9E8F /* HubFlowsUITests.swift */,
|
||||||
|
);
|
||||||
|
name = MatchLiveTvUITests;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
7E992B84100649D8BE8507F7 = {
|
7E992B84100649D8BE8507F7 = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
DDEF729FE3334F8998C4A31C /* MatchLiveTv */,
|
DDEF729FE3334F8998C4A31C /* MatchLiveTv */,
|
||||||
6987D3982FAD44A6869B9FD6 /* MatchLiveTvTests */,
|
6987D3982FAD44A6869B9FD6 /* MatchLiveTvTests */,
|
||||||
|
2A180796A5B4C3D2E1F0A98B /* MatchLiveTvUITests */,
|
||||||
0ADC6126267C410F9B60B426 /* Products */,
|
0ADC6126267C410F9B60B426 /* Products */,
|
||||||
);
|
);
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -364,6 +384,22 @@
|
|||||||
productReference = 3FC0CC8155BE469ABB420DC5 /* MatchLiveTvTests.xctest */;
|
productReference = 3FC0CC8155BE469ABB420DC5 /* MatchLiveTvTests.xctest */;
|
||||||
productType = "com.apple.product-type.bundle.unit-test";
|
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 */ = {
|
F54F6C97361C4E8A96AEDD11 /* MatchLiveTv */ = {
|
||||||
isa = PBXNativeTarget;
|
isa = PBXNativeTarget;
|
||||||
buildConfigurationList = 117DCD84E04B4B03A2BFFFEA /* Build configuration list for PBXNativeTarget "MatchLiveTv" */;
|
buildConfigurationList = 117DCD84E04B4B03A2BFFFEA /* Build configuration list for PBXNativeTarget "MatchLiveTv" */;
|
||||||
@@ -414,6 +450,7 @@
|
|||||||
targets = (
|
targets = (
|
||||||
F54F6C97361C4E8A96AEDD11 /* MatchLiveTv */,
|
F54F6C97361C4E8A96AEDD11 /* MatchLiveTv */,
|
||||||
20C695DBFB4042879308B0F5 /* MatchLiveTvTests */,
|
20C695DBFB4042879308B0F5 /* MatchLiveTvTests */,
|
||||||
|
6D5C4B3A29180796A5B4C3D2 /* MatchLiveTvUITests */,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
/* End PBXProject section */
|
/* End PBXProject section */
|
||||||
@@ -531,6 +568,14 @@
|
|||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
5C4B3A29180796A5B4C3D2E1 /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
8F7E6D5C4B3A29180796A5B4 /* HubFlowsUITests.swift in Sources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
/* End PBXSourcesBuildPhase section */
|
/* End PBXSourcesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXTargetDependency section */
|
/* Begin PBXTargetDependency section */
|
||||||
@@ -539,6 +584,11 @@
|
|||||||
target = F54F6C97361C4E8A96AEDD11 /* MatchLiveTv */;
|
target = F54F6C97361C4E8A96AEDD11 /* MatchLiveTv */;
|
||||||
targetProxy = 58E3C5EC752E497FBFD32D78 /* PBXContainerItemProxy */;
|
targetProxy = 58E3C5EC752E497FBFD32D78 /* PBXContainerItemProxy */;
|
||||||
};
|
};
|
||||||
|
3A29180796A5B4C3D2E1F0A9 /* PBXTargetDependency */ = {
|
||||||
|
isa = PBXTargetDependency;
|
||||||
|
target = F54F6C97361C4E8A96AEDD11 /* MatchLiveTv */;
|
||||||
|
targetProxy = 4B3A29180796A5B4C3D2E1F0 /* PBXContainerItemProxy */;
|
||||||
|
};
|
||||||
/* End PBXTargetDependency section */
|
/* End PBXTargetDependency section */
|
||||||
|
|
||||||
/* Begin XCBuildConfiguration section */
|
/* Begin XCBuildConfiguration section */
|
||||||
@@ -627,10 +677,11 @@
|
|||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 32;
|
CURRENT_PROJECT_VERSION = 35;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
INFOPLIST_KEY_API_BASE_URL = "$(API_BASE_URL)";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
|
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_BUNDLE_IDENTIFIER = "com.matchlivetv.match-live-tv.tests";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
@@ -647,7 +698,7 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 32;
|
CURRENT_PROJECT_VERSION = 35;
|
||||||
DEVELOPMENT_TEAM = S8Q9TWBRG5;
|
DEVELOPMENT_TEAM = S8Q9TWBRG5;
|
||||||
GENERATE_INFOPLIST_FILE = NO;
|
GENERATE_INFOPLIST_FILE = NO;
|
||||||
INFOPLIST_FILE = MatchLiveTv/Resources/Info.plist;
|
INFOPLIST_FILE = MatchLiveTv/Resources/Info.plist;
|
||||||
@@ -658,7 +709,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 2.0.10;
|
MARKETING_VERSION = 2.0.12;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "com.matchlivetv.match-live-tv";
|
PRODUCT_BUNDLE_IDENTIFIER = "com.matchlivetv.match-live-tv";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
@@ -676,7 +727,7 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 32;
|
CURRENT_PROJECT_VERSION = 35;
|
||||||
DEVELOPMENT_TEAM = S8Q9TWBRG5;
|
DEVELOPMENT_TEAM = S8Q9TWBRG5;
|
||||||
ENABLE_TESTABILITY = YES;
|
ENABLE_TESTABILITY = YES;
|
||||||
GENERATE_INFOPLIST_FILE = NO;
|
GENERATE_INFOPLIST_FILE = NO;
|
||||||
@@ -688,7 +739,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 2.0.10;
|
MARKETING_VERSION = 2.0.12;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "com.matchlivetv.match-live-tv";
|
PRODUCT_BUNDLE_IDENTIFIER = "com.matchlivetv.match-live-tv";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
@@ -705,10 +756,11 @@
|
|||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 32;
|
CURRENT_PROJECT_VERSION = 35;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
INFOPLIST_KEY_API_BASE_URL = "$(API_BASE_URL)";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
|
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_BUNDLE_IDENTIFIER = "com.matchlivetv.match-live-tv.tests";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
@@ -718,6 +770,44 @@
|
|||||||
};
|
};
|
||||||
name = Debug;
|
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 */
|
/* End XCBuildConfiguration section */
|
||||||
|
|
||||||
/* Begin XCConfigurationList section */
|
/* Begin XCConfigurationList section */
|
||||||
@@ -748,6 +838,15 @@
|
|||||||
defaultConfigurationIsVisible = 0;
|
defaultConfigurationIsVisible = 0;
|
||||||
defaultConfigurationName = Release;
|
defaultConfigurationName = Release;
|
||||||
};
|
};
|
||||||
|
F01796A5B4C3D2E1F0A98D7E /* Build configuration list for PBXNativeTarget "MatchLiveTvUITests" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
19180796A5B4C3D2E1F0A98C /* Debug */,
|
||||||
|
081796A5B4C3D2E1F0A98D7E /* Release */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
/* End XCConfigurationList section */
|
/* End XCConfigurationList section */
|
||||||
|
|
||||||
/* Begin XCRemoteSwiftPackageReference section */
|
/* Begin XCRemoteSwiftPackageReference section */
|
||||||
|
|||||||
@@ -52,6 +52,16 @@
|
|||||||
ReferencedContainer = "container:MatchLiveTv.xcodeproj">
|
ReferencedContainer = "container:MatchLiveTv.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
</TestableReference>
|
</TestableReference>
|
||||||
|
<TestableReference
|
||||||
|
skipped = "NO">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "6D5C4B3A29180796A5B4C3D2"
|
||||||
|
BuildableName = "MatchLiveTvUITests.xctest"
|
||||||
|
BlueprintName = "MatchLiveTvUITests"
|
||||||
|
ReferencedContainer = "container:MatchLiveTv.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</TestableReference>
|
||||||
</Testables>
|
</Testables>
|
||||||
</TestAction>
|
</TestAction>
|
||||||
<LaunchAction
|
<LaunchAction
|
||||||
|
|||||||
@@ -2,6 +2,10 @@ import Foundation
|
|||||||
|
|
||||||
enum AppConfig {
|
enum AppConfig {
|
||||||
static var apiBaseUrl: String {
|
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
|
let raw = Bundle.main.object(forInfoDictionaryKey: "API_BASE_URL") as? String
|
||||||
?? "https://www.matchlivetv.it"
|
?? "https://www.matchlivetv.it"
|
||||||
return raw.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
|
return raw.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
|
||||||
|
|||||||
@@ -1763,6 +1763,7 @@ struct LanguagePickerView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.accessibilityIdentifier("language.\(lang.rawValue)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationTitle(L10n.t("language.label"))
|
.navigationTitle(L10n.t("language.label"))
|
||||||
|
|||||||
@@ -16,8 +16,21 @@ final class TokenStore: ObservableObject {
|
|||||||
private let service = "com.matchlivetv.match-live-tv.auth"
|
private let service = "com.matchlivetv.match-live-tv.auth"
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
session = loadSession()
|
if ProcessInfo.processInfo.arguments.contains("--uitesting-reset") {
|
||||||
activeTeamId = defaults.string(forKey: Keys.activeTeamId)
|
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) {
|
func saveSession(_ session: StoredSession) {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>2.0.10</string>
|
<string>2.0.12</string>
|
||||||
<key>CFBundleURLTypes</key>
|
<key>CFBundleURLTypes</key>
|
||||||
<array>
|
<array>
|
||||||
<dict>
|
<dict>
|
||||||
@@ -32,7 +32,7 @@
|
|||||||
</dict>
|
</dict>
|
||||||
</array>
|
</array>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>32</string>
|
<string>35</string>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>NSCameraUsageDescription</key>
|
<key>NSCameraUsageDescription</key>
|
||||||
@@ -41,10 +41,6 @@
|
|||||||
<string>Match Live TV usa il microfono per l'audio della diretta.</string>
|
<string>Match Live TV usa il microfono per l'audio della diretta.</string>
|
||||||
<key>NSPhotoLibraryUsageDescription</key>
|
<key>NSPhotoLibraryUsageDescription</key>
|
||||||
<string>Match Live TV usa le foto per i loghi delle squadre.</string>
|
<string>Match Live TV usa le foto per i loghi delle squadre.</string>
|
||||||
<key>UIBackgroundModes</key>
|
|
||||||
<array>
|
|
||||||
<string>audio</string>
|
|
||||||
</array>
|
|
||||||
<key>UILaunchScreen</key>
|
<key>UILaunchScreen</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>UIColorName</key>
|
<key>UIColorName</key>
|
||||||
|
|||||||
@@ -30,9 +30,11 @@ struct AccountScreen: View {
|
|||||||
Image(systemName: "chevron.left")
|
Image(systemName: "chevron.left")
|
||||||
.foregroundStyle(MatchColors.textSecondary)
|
.foregroundStyle(MatchColors.textSecondary)
|
||||||
}
|
}
|
||||||
|
.accessibilityIdentifier("account.back")
|
||||||
Text(L10n.t("account.title"))
|
Text(L10n.t("account.title"))
|
||||||
.font(MatchTypography.headlineMedium)
|
.font(MatchTypography.headlineMedium)
|
||||||
.foregroundStyle(.white)
|
.foregroundStyle(.white)
|
||||||
|
.accessibilityIdentifier("account.title")
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 16)
|
.padding(.horizontal, 16)
|
||||||
@@ -66,6 +68,7 @@ struct AccountScreen: View {
|
|||||||
enabled: !loadingProfile && !name.trimmingCharacters(in: .whitespaces).isEmpty && !savingProfile,
|
enabled: !loadingProfile && !name.trimmingCharacters(in: .whitespaces).isEmpty && !savingProfile,
|
||||||
loading: savingProfile
|
loading: savingProfile
|
||||||
)
|
)
|
||||||
|
.accessibilityIdentifier("account.save.profile")
|
||||||
|
|
||||||
Text(L10n.t("account.password.heading"))
|
Text(L10n.t("account.password.heading"))
|
||||||
.font(MatchTypography.titleMedium)
|
.font(MatchTypography.titleMedium)
|
||||||
@@ -75,17 +78,20 @@ struct AccountScreen: View {
|
|||||||
MatchAccountSecureField(
|
MatchAccountSecureField(
|
||||||
title: L10n.t("account.current.password"),
|
title: L10n.t("account.current.password"),
|
||||||
text: $currentPassword,
|
text: $currentPassword,
|
||||||
visible: $passwordVisible
|
visible: $passwordVisible,
|
||||||
|
accessibilityId: "account.password.current"
|
||||||
)
|
)
|
||||||
MatchAccountSecureField(
|
MatchAccountSecureField(
|
||||||
title: L10n.t("account.new.password"),
|
title: L10n.t("account.new.password"),
|
||||||
text: $newPassword,
|
text: $newPassword,
|
||||||
visible: $passwordVisible
|
visible: $passwordVisible,
|
||||||
|
accessibilityId: "account.password.new"
|
||||||
)
|
)
|
||||||
MatchAccountSecureField(
|
MatchAccountSecureField(
|
||||||
title: L10n.t("account.confirm.password"),
|
title: L10n.t("account.confirm.password"),
|
||||||
text: $confirmPassword,
|
text: $confirmPassword,
|
||||||
visible: $passwordVisible
|
visible: $passwordVisible,
|
||||||
|
accessibilityId: "account.password.confirm"
|
||||||
)
|
)
|
||||||
|
|
||||||
if let passwordError {
|
if let passwordError {
|
||||||
@@ -101,12 +107,14 @@ struct AccountScreen: View {
|
|||||||
enabled: !currentPassword.isEmpty && !newPassword.isEmpty && !confirmPassword.isEmpty && !savingPassword,
|
enabled: !currentPassword.isEmpty && !newPassword.isEmpty && !confirmPassword.isEmpty && !savingPassword,
|
||||||
loading: savingPassword
|
loading: savingPassword
|
||||||
)
|
)
|
||||||
|
.accessibilityIdentifier("account.save.password")
|
||||||
|
|
||||||
MatchSecondaryButton(
|
MatchSecondaryButton(
|
||||||
label: L10n.t("action.logout"),
|
label: L10n.t("action.logout"),
|
||||||
action: logout,
|
action: logout,
|
||||||
enabled: !loggingOut
|
enabled: !loggingOut
|
||||||
)
|
)
|
||||||
|
.accessibilityIdentifier("account.logout")
|
||||||
.padding(.top, 24)
|
.padding(.top, 24)
|
||||||
}
|
}
|
||||||
.padding(24)
|
.padding(24)
|
||||||
@@ -214,6 +222,8 @@ private struct MatchAccountSecureField: View {
|
|||||||
@Binding var text: String
|
@Binding var text: String
|
||||||
@Binding var visible: Bool
|
@Binding var visible: Bool
|
||||||
|
|
||||||
|
var accessibilityId: String = ""
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading, spacing: 6) {
|
VStack(alignment: .leading, spacing: 6) {
|
||||||
Text(title)
|
Text(title)
|
||||||
@@ -229,6 +239,7 @@ private struct MatchAccountSecureField: View {
|
|||||||
}
|
}
|
||||||
.textInputAutocapitalization(.never)
|
.textInputAutocapitalization(.never)
|
||||||
.autocorrectionDisabled()
|
.autocorrectionDisabled()
|
||||||
|
.accessibilityIdentifier(accessibilityId)
|
||||||
Button(action: { visible.toggle() }) {
|
Button(action: { visible.toggle() }) {
|
||||||
Image(systemName: visible ? "eye.slash" : "eye")
|
Image(systemName: visible ? "eye.slash" : "eye")
|
||||||
.foregroundStyle(MatchColors.textSecondary)
|
.foregroundStyle(MatchColors.textSecondary)
|
||||||
|
|||||||
@@ -194,16 +194,19 @@ struct BroadcastControlsOverlay: View {
|
|||||||
action: { pendingConfirm = .shareLive },
|
action: { pendingConfirm = .shareLive },
|
||||||
enabled: shareLiveEnabled
|
enabled: shareLiveEnabled
|
||||||
)
|
)
|
||||||
|
.accessibilityIdentifier("broadcast.share")
|
||||||
SideIconButton(
|
SideIconButton(
|
||||||
systemName: "video.fill",
|
systemName: "video.fill",
|
||||||
accessibilityLabel: L10n.t("broadcast.share.regia.cd"),
|
accessibilityLabel: L10n.t("broadcast.share.regia.cd"),
|
||||||
action: { pendingConfirm = .shareRegia }
|
action: { pendingConfirm = .shareRegia }
|
||||||
)
|
)
|
||||||
|
.accessibilityIdentifier("broadcast.regia")
|
||||||
SideIconButton(
|
SideIconButton(
|
||||||
systemName: "slider.horizontal.3",
|
systemName: "slider.horizontal.3",
|
||||||
accessibilityLabel: L10n.t("broadcast.min.quality.cd"),
|
accessibilityLabel: L10n.t("broadcast.min.quality.cd"),
|
||||||
action: { showMinQualityPicker = true }
|
action: { showMinQualityPicker = true }
|
||||||
)
|
)
|
||||||
|
.accessibilityIdentifier("broadcast.abr")
|
||||||
if let onCloseSet {
|
if let onCloseSet {
|
||||||
SideIconButton(
|
SideIconButton(
|
||||||
systemName: "checkmark",
|
systemName: "checkmark",
|
||||||
@@ -231,18 +234,21 @@ struct BroadcastControlsOverlay: View {
|
|||||||
action: { pendingConfirm = .pauseOrResume },
|
action: { pendingConfirm = .pauseOrResume },
|
||||||
highlighted: isPaused
|
highlighted: isPaused
|
||||||
)
|
)
|
||||||
|
.accessibilityIdentifier("broadcast.pause")
|
||||||
SideIconButton(
|
SideIconButton(
|
||||||
systemName: audioMuted ? "speaker.slash.fill" : "speaker.wave.2.fill",
|
systemName: audioMuted ? "speaker.slash.fill" : "speaker.wave.2.fill",
|
||||||
accessibilityLabel: audioMuted ? L10n.t("broadcast.unmute.cd") : L10n.t("broadcast.mute.cd"),
|
accessibilityLabel: audioMuted ? L10n.t("broadcast.unmute.cd") : L10n.t("broadcast.mute.cd"),
|
||||||
action: { pendingConfirm = .toggleMute },
|
action: { pendingConfirm = .toggleMute },
|
||||||
highlighted: audioMuted
|
highlighted: audioMuted
|
||||||
)
|
)
|
||||||
|
.accessibilityIdentifier("broadcast.mute")
|
||||||
SideIconButton(
|
SideIconButton(
|
||||||
systemName: "stop.fill",
|
systemName: "stop.fill",
|
||||||
accessibilityLabel: L10n.t("broadcast.terminate.cd"),
|
accessibilityLabel: L10n.t("broadcast.terminate.cd"),
|
||||||
action: { pendingConfirm = .terminate },
|
action: { pendingConfirm = .terminate },
|
||||||
danger: true
|
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)
|
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)
|
ScoreIconButton(label: "−", tooltip: L10n.t("broadcast.tooltip.remove.point", teamSide), action: onMinus)
|
||||||
} else {
|
} else {
|
||||||
ScoreIconButton(label: "−", tooltip: L10n.t("broadcast.tooltip.remove.point", teamSide), action: onMinus)
|
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)
|
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 showBasketButtons {
|
||||||
if let onPlus2 {
|
if let onPlus2 {
|
||||||
ScoreIconButton(label: "+2", tooltip: L10n.t("broadcast.tooltip.plus.side", 2, teamSide), action: onPlus2, primary: true)
|
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")
|
Image(systemName: "chevron.left")
|
||||||
.foregroundStyle(MatchColors.textSecondary)
|
.foregroundStyle(MatchColors.textSecondary)
|
||||||
}
|
}
|
||||||
|
.accessibilityIdentifier("forgot.back")
|
||||||
Text(L10n.t("forgot.password.title"))
|
Text(L10n.t("forgot.password.title"))
|
||||||
.font(MatchTypography.headlineMedium)
|
.font(MatchTypography.headlineMedium)
|
||||||
.foregroundStyle(.white)
|
.foregroundStyle(.white)
|
||||||
@@ -64,6 +65,7 @@ struct ForgotPasswordScreen: View {
|
|||||||
enabled: !email.trimmingCharacters(in: .whitespaces).isEmpty && !loading && !success,
|
enabled: !email.trimmingCharacters(in: .whitespaces).isEmpty && !loading && !success,
|
||||||
loading: loading
|
loading: loading
|
||||||
)
|
)
|
||||||
|
.accessibilityIdentifier("forgot.submit")
|
||||||
}
|
}
|
||||||
.padding(24)
|
.padding(24)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ struct LoginScreen: View {
|
|||||||
.foregroundStyle(MatchColors.textSecondary)
|
.foregroundStyle(MatchColors.textSecondary)
|
||||||
}
|
}
|
||||||
.accessibilityLabel(L10n.t("language.label"))
|
.accessibilityLabel(L10n.t("language.label"))
|
||||||
|
.accessibilityIdentifier("login.language")
|
||||||
}
|
}
|
||||||
.padding(.top, 8)
|
.padding(.top, 8)
|
||||||
MatchLiveWordmark(showSlogan: true)
|
MatchLiveWordmark(showSlogan: true)
|
||||||
@@ -57,6 +58,7 @@ struct LoginScreen: View {
|
|||||||
.foregroundStyle(MatchColors.textSecondary)
|
.foregroundStyle(MatchColors.textSecondary)
|
||||||
.underline()
|
.underline()
|
||||||
}
|
}
|
||||||
|
.accessibilityIdentifier("login.forgot")
|
||||||
}
|
}
|
||||||
.padding(.top, 12)
|
.padding(.top, 12)
|
||||||
if let error {
|
if let error {
|
||||||
@@ -71,6 +73,7 @@ struct LoginScreen: View {
|
|||||||
enabled: !email.isEmpty && !password.isEmpty,
|
enabled: !email.isEmpty && !password.isEmpty,
|
||||||
loading: loading
|
loading: loading
|
||||||
)
|
)
|
||||||
|
.accessibilityIdentifier("login.submit")
|
||||||
.padding(.top, 32)
|
.padding(.top, 32)
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 24)
|
.padding(.horizontal, 24)
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ struct MatchesScreen: View {
|
|||||||
@State private var showSchedule = false
|
@State private var showSchedule = false
|
||||||
@State private var showTeamPicker = false
|
@State private var showTeamPicker = false
|
||||||
@State private var resumeMatch: Match?
|
@State private var resumeMatch: Match?
|
||||||
|
@State private var configureMatch: Match?
|
||||||
|
@State private var pendingConfigureMatch: Match?
|
||||||
@State private var deleteMatch: Match?
|
@State private var deleteMatch: Match?
|
||||||
@State private var snackbar: String?
|
@State private var snackbar: String?
|
||||||
@State private var showLanguagePicker = false
|
@State private var showLanguagePicker = false
|
||||||
@@ -35,15 +37,19 @@ struct MatchesScreen: View {
|
|||||||
} label: {
|
} label: {
|
||||||
Image(systemName: "globe")
|
Image(systemName: "globe")
|
||||||
.foregroundStyle(MatchColors.textSecondary)
|
.foregroundStyle(MatchColors.textSecondary)
|
||||||
|
.frame(minWidth: 44, minHeight: 44)
|
||||||
}
|
}
|
||||||
.accessibilityLabel(L10n.t("language.label"))
|
.accessibilityLabel(L10n.t("language.label"))
|
||||||
|
.accessibilityIdentifier("matches.language")
|
||||||
Button {
|
Button {
|
||||||
onOpenAccount()
|
onOpenAccount()
|
||||||
} label: {
|
} label: {
|
||||||
Image(systemName: "person.crop.circle")
|
Image(systemName: "person.crop.circle")
|
||||||
.foregroundStyle(MatchColors.textSecondary)
|
.foregroundStyle(MatchColors.textSecondary)
|
||||||
|
.frame(minWidth: 44, minHeight: 44)
|
||||||
}
|
}
|
||||||
.accessibilityLabel(L10n.t("account.title"))
|
.accessibilityLabel(L10n.t("account.title"))
|
||||||
|
.accessibilityIdentifier("matches.account")
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 16)
|
.padding(.horizontal, 16)
|
||||||
.padding(.vertical, 8)
|
.padding(.vertical, 8)
|
||||||
@@ -91,6 +97,7 @@ struct MatchesScreen: View {
|
|||||||
.font(MatchTypography.bodyMedium)
|
.font(MatchTypography.bodyMedium)
|
||||||
.foregroundStyle(MatchColors.textSecondary)
|
.foregroundStyle(MatchColors.textSecondary)
|
||||||
MatchPrimaryButton(label: L10n.t("matches.new").uppercased(), action: { showNewMatch = true })
|
MatchPrimaryButton(label: L10n.t("matches.new").uppercased(), action: { showNewMatch = true })
|
||||||
|
.accessibilityIdentifier("matches.new")
|
||||||
if let activeTeam {
|
if let activeTeam {
|
||||||
TeamPickerBar(
|
TeamPickerBar(
|
||||||
team: activeTeam,
|
team: activeTeam,
|
||||||
@@ -151,14 +158,16 @@ struct MatchesScreen: View {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
.task(id: refreshToken) { reload(showSpinner: refreshToken == 0) }
|
.task(id: refreshToken) { reload(showSpinner: refreshToken == 0) }
|
||||||
.alert(L10n.t("sheet.configure.now.title"), isPresented: Binding(get: { resumeMatch != nil }, set: { if !$0 { resumeMatch = nil } })) {
|
.alert(
|
||||||
Button(L10n.t("sheet.resume.camera")) {
|
L10n.t("sheet.configure.now.title"),
|
||||||
if let match = resumeMatch { resumeBroadcast(match) }
|
isPresented: Binding(get: { configureMatch != nil }, set: { if !$0 { configureMatch = nil } })
|
||||||
resumeMatch = nil
|
) {
|
||||||
|
Button(L10n.t("sheet.configure")) {
|
||||||
|
if let match = configureMatch { onOpenSetup(match.id) }
|
||||||
|
configureMatch = nil
|
||||||
}
|
}
|
||||||
Button(L10n.t("sheet.configure"), role: .cancel) {
|
Button(L10n.t("sheet.later"), role: .cancel) {
|
||||||
if let match = resumeMatch { onOpenSetup(match.id) }
|
configureMatch = nil
|
||||||
resumeMatch = nil
|
|
||||||
}
|
}
|
||||||
} message: {
|
} message: {
|
||||||
Text(L10n.t("sheet.configure.now.body"))
|
Text(L10n.t("sheet.configure.now.body"))
|
||||||
@@ -201,10 +210,19 @@ struct MatchesScreen: View {
|
|||||||
.presentationDetents([.medium])
|
.presentationDetents([.medium])
|
||||||
.presentationDragIndicator(.visible)
|
.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
|
ScheduleMatchSheet(container: container, teamId: activeTeam?.id) { match in
|
||||||
|
pendingConfigureMatch = match
|
||||||
|
snackbar = L10n.t("matches.msg.scheduled.ok")
|
||||||
|
reload(showSpinner: false)
|
||||||
showSchedule = false
|
showSchedule = false
|
||||||
onOpenSetup(match.id)
|
} onFailed: { error in
|
||||||
|
snackbar = UserFacingError.message(for: error) ?? L10n.t("matches.msg.schedule.error")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.sheet(isPresented: $showTeamPicker) {
|
.sheet(isPresented: $showTeamPicker) {
|
||||||
@@ -229,6 +247,29 @@ struct MatchesScreen: View {
|
|||||||
ProgressView().tint(MatchColors.primaryRed)
|
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) {
|
.overlay(alignment: .bottom) {
|
||||||
if let snackbar {
|
if let snackbar {
|
||||||
Text(snackbar)
|
Text(snackbar)
|
||||||
@@ -379,6 +420,7 @@ private struct ActiveSessionBanner: View {
|
|||||||
.background(MatchColors.primaryRed.opacity(0.15), in: RoundedRectangle(cornerRadius: 12))
|
.background(MatchColors.primaryRed.opacity(0.15), in: RoundedRectangle(cornerRadius: 12))
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
|
.accessibilityIdentifier("matches.resume.banner")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -523,12 +565,14 @@ private struct NewMatchSheet: View {
|
|||||||
subtitle: L10n.t("sheet.schedule.option.sub"),
|
subtitle: L10n.t("sheet.schedule.option.sub"),
|
||||||
action: onSchedule
|
action: onSchedule
|
||||||
)
|
)
|
||||||
|
.accessibilityIdentifier("sheet.schedule.option")
|
||||||
SheetOptionTile(
|
SheetOptionTile(
|
||||||
systemImage: "play.circle",
|
systemImage: "play.circle",
|
||||||
title: L10n.t("sheet.quick.option"),
|
title: L10n.t("sheet.quick.option"),
|
||||||
subtitle: L10n.t("sheet.quick.option.sub"),
|
subtitle: L10n.t("sheet.quick.option.sub"),
|
||||||
action: onQuickStart
|
action: onQuickStart
|
||||||
)
|
)
|
||||||
|
.accessibilityIdentifier("sheet.quick.option")
|
||||||
}
|
}
|
||||||
.padding(.top, 20)
|
.padding(.top, 20)
|
||||||
Spacer(minLength: 24)
|
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 {
|
private struct ScheduleMatchSheet: View {
|
||||||
@ObservedObject var container: AppContainer
|
@ObservedObject var container: AppContainer
|
||||||
let teamId: String?
|
let teamId: String?
|
||||||
let onCreated: (Match) -> Void
|
let onCreated: (Match) -> Void
|
||||||
|
var onFailed: (Error) -> Void = { _ in }
|
||||||
@Environment(\.dismiss) private var dismiss
|
@Environment(\.dismiss) private var dismiss
|
||||||
@State private var opponent = ""
|
@State private var opponent = ""
|
||||||
@State private var location = ""
|
@State private var location = ""
|
||||||
@State private var date = Date().addingTimeInterval(86400)
|
@State private var date = Date().addingTimeInterval(86400)
|
||||||
|
@State private var saving = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
@@ -593,14 +691,15 @@ private struct ScheduleMatchSheet: View {
|
|||||||
}
|
}
|
||||||
ToolbarItem(placement: .confirmationAction) {
|
ToolbarItem(placement: .confirmationAction) {
|
||||||
Button(L10n.t("sheet.save.schedule")) { create() }
|
Button(L10n.t("sheet.save.schedule")) { create() }
|
||||||
.disabled(opponent.isEmpty || teamId == nil)
|
.disabled(opponent.isEmpty || teamId == nil || saving)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func create() {
|
private func create() {
|
||||||
guard let teamId else { return }
|
guard let teamId, !saving else { return }
|
||||||
|
saving = true
|
||||||
Task {
|
Task {
|
||||||
do {
|
do {
|
||||||
let match = try await container.matchRepository.createScheduledMatch(
|
let match = try await container.matchRepository.createScheduledMatch(
|
||||||
@@ -611,7 +710,8 @@ private struct ScheduleMatchSheet: View {
|
|||||||
)
|
)
|
||||||
onCreated(match)
|
onCreated(match)
|
||||||
} catch {
|
} 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 PhotosUI
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import UIKit
|
||||||
|
|
||||||
struct MatchCoverSection: View {
|
struct MatchCoverSection: View {
|
||||||
let effectiveCoverUrl: String?
|
let effectiveCoverUrl: String?
|
||||||
@@ -29,6 +30,7 @@ struct MatchCoverSection: View {
|
|||||||
.font(MatchTypography.bodyMedium)
|
.font(MatchTypography.bodyMedium)
|
||||||
.foregroundStyle(MatchColors.textSecondary)
|
.foregroundStyle(MatchColors.textSecondary)
|
||||||
.padding(.top, 4)
|
.padding(.top, 4)
|
||||||
|
.accessibilityIdentifier("wizard.cover.source")
|
||||||
|
|
||||||
Group {
|
Group {
|
||||||
if let localCoverImage {
|
if let localCoverImage {
|
||||||
@@ -68,8 +70,20 @@ struct MatchCoverSection: View {
|
|||||||
.overlay(RoundedRectangle(cornerRadius: 8).stroke(MatchColors.outline, lineWidth: 1))
|
.overlay(RoundedRectangle(cornerRadius: 8).stroke(MatchColors.outline, lineWidth: 1))
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
|
.accessibilityIdentifier("wizard.cover.change")
|
||||||
.padding(.top, 12)
|
.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 {
|
if hasMatchOverride || localCoverImage != nil {
|
||||||
MatchSecondaryButton(
|
MatchSecondaryButton(
|
||||||
label: L10n.t("wizard.match.cover.reset"),
|
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)
|
MatchPrimaryButton(label: L10n.t("wizard.action.next"), action: saveAndContinue, enabled: !saving, loading: saving)
|
||||||
|
.accessibilityIdentifier("wizard.next")
|
||||||
.padding(.top, 32)
|
.padding(.top, 32)
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 20)
|
.padding(.horizontal, 20)
|
||||||
|
|||||||
@@ -92,7 +92,8 @@ struct StepNetworkTestScreen: View {
|
|||||||
if testCompleted, let shareUrl {
|
if testCompleted, let shareUrl {
|
||||||
WizardReadOnlyField(
|
WizardReadOnlyField(
|
||||||
label: currentSession.platform == "youtube" ? L10n.t("wizard.network.link.youtube.label") : L10n.t("wizard.network.link.live.label"),
|
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)
|
.padding(.top, 16)
|
||||||
|
|
||||||
@@ -125,6 +126,7 @@ struct StepNetworkTestScreen: View {
|
|||||||
action: runTest,
|
action: runTest,
|
||||||
enabled: !testing
|
enabled: !testing
|
||||||
)
|
)
|
||||||
|
.accessibilityIdentifier("wizard.network.test")
|
||||||
.padding(.top, 24)
|
.padding(.top, 24)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,12 +137,14 @@ struct StepNetworkTestScreen: View {
|
|||||||
HStack(spacing: spacing) {
|
HStack(spacing: spacing) {
|
||||||
MatchSecondaryButton(label: L10n.t("wizard.action.back"), action: onBack, enabled: !starting)
|
MatchSecondaryButton(label: L10n.t("wizard.action.back"), action: onBack, enabled: !starting)
|
||||||
.frame(width: backWidth)
|
.frame(width: backWidth)
|
||||||
|
.accessibilityIdentifier("wizard.back")
|
||||||
MatchPrimaryButton(
|
MatchPrimaryButton(
|
||||||
label: L10n.t("wizard.action.start"),
|
label: L10n.t("wizard.action.start"),
|
||||||
action: startLive,
|
action: startLive,
|
||||||
enabled: ready,
|
enabled: ready,
|
||||||
loading: starting
|
loading: starting
|
||||||
)
|
)
|
||||||
|
.accessibilityIdentifier("wizard.start")
|
||||||
.frame(width: forwardWidth)
|
.frame(width: forwardWidth)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ struct StepTransmissionScreen: View {
|
|||||||
|
|
||||||
@State private var team: Team?
|
@State private var team: Team?
|
||||||
@State private var platform = "matchlivetv"
|
@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
|
@State private var creating = false
|
||||||
|
|
||||||
private var youtubeReady: Bool {
|
private var youtubeReady: Bool {
|
||||||
@@ -42,6 +42,7 @@ struct StepTransmissionScreen: View {
|
|||||||
selected: platform == "matchlivetv",
|
selected: platform == "matchlivetv",
|
||||||
onClick: { platform = "matchlivetv" }
|
onClick: { platform = "matchlivetv" }
|
||||||
)
|
)
|
||||||
|
.accessibilityIdentifier("wizard.platform.site")
|
||||||
.padding(.top, 12)
|
.padding(.top, 12)
|
||||||
|
|
||||||
WizardPlatformCard(
|
WizardPlatformCard(
|
||||||
@@ -70,6 +71,7 @@ struct StepTransmissionScreen: View {
|
|||||||
selected: privacy == "unlisted",
|
selected: privacy == "unlisted",
|
||||||
action: { privacy = "unlisted" }
|
action: { privacy = "unlisted" }
|
||||||
)
|
)
|
||||||
|
.accessibilityIdentifier("wizard.privacy.unlisted")
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
}
|
}
|
||||||
.padding(.top, 12)
|
.padding(.top, 12)
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ struct TeamBrandingRow: View {
|
|||||||
.font(MatchTypography.titleMedium)
|
.font(MatchTypography.titleMedium)
|
||||||
.padding(8)
|
.padding(8)
|
||||||
.overlay(RoundedRectangle(cornerRadius: 8).stroke(MatchColors.outline, lineWidth: 1))
|
.overlay(RoundedRectangle(cornerRadius: 8).stroke(MatchColors.outline, lineWidth: 1))
|
||||||
|
.accessibilityIdentifier("wizard.opponent")
|
||||||
} else {
|
} else {
|
||||||
Text(teamName.isEmpty ? L10n.t("wizard.branding.team.fallback.name") : teamName)
|
Text(teamName.isEmpty ? L10n.t("wizard.branding.team.fallback.name") : teamName)
|
||||||
.font(MatchTypography.titleMedium)
|
.font(MatchTypography.titleMedium)
|
||||||
|
|||||||
@@ -37,9 +37,11 @@ struct WizardFooterButtons: View {
|
|||||||
if showBack {
|
if showBack {
|
||||||
MatchSecondaryButton(label: L10n.t("wizard.action.back"), action: onBack)
|
MatchSecondaryButton(label: L10n.t("wizard.action.back"), action: onBack)
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
|
.accessibilityIdentifier("wizard.back")
|
||||||
}
|
}
|
||||||
MatchPrimaryButton(label: forwardLabel, action: onForward, loading: forwardLoading)
|
MatchPrimaryButton(label: forwardLabel, action: onForward, loading: forwardLoading)
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
|
.accessibilityIdentifier("wizard.next")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -66,11 +68,18 @@ struct WizardOutlinedField: View {
|
|||||||
struct WizardReadOnlyField: View {
|
struct WizardReadOnlyField: View {
|
||||||
let label: String
|
let label: String
|
||||||
let value: String
|
let value: String
|
||||||
|
var valueIdentifier: String? = nil
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
Text(label).font(MatchTypography.bodyMedium)
|
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)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
.padding(14)
|
.padding(14)
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ struct WizardShellScreen: View {
|
|||||||
Image(systemName: "xmark").foregroundStyle(.white)
|
Image(systemName: "xmark").foregroundStyle(.white)
|
||||||
}
|
}
|
||||||
.accessibilityLabel(L10n.t("common.close"))
|
.accessibilityLabel(L10n.t("common.close"))
|
||||||
|
.accessibilityIdentifier("wizard.close")
|
||||||
Text(wizardStepTitle(currentStep))
|
Text(wizardStepTitle(currentStep))
|
||||||
.font(MatchTypography.titleMedium)
|
.font(MatchTypography.titleMedium)
|
||||||
Spacer(minLength: 0)
|
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 = """
|
app_settings = """
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 32;
|
CURRENT_PROJECT_VERSION = 35;
|
||||||
GENERATE_INFOPLIST_FILE = NO;
|
GENERATE_INFOPLIST_FILE = NO;
|
||||||
INFOPLIST_FILE = MatchLiveTv/Resources/Info.plist;
|
INFOPLIST_FILE = MatchLiveTv/Resources/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||||
@@ -107,7 +107,7 @@ app_settings = """
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 2.0.10;
|
MARKETING_VERSION = 2.0.12;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.matchlivetv.match-live-tv;
|
PRODUCT_BUNDLE_IDENTIFIER = com.matchlivetv.match-live-tv;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
API_BASE_URL = "https://www.matchlivetv.it";
|
API_BASE_URL = "https://www.matchlivetv.it";
|
||||||
@@ -122,10 +122,10 @@ app_debug_settings = app_settings + """
|
|||||||
test_settings = """
|
test_settings = """
|
||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 32;
|
CURRENT_PROJECT_VERSION = 35;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
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_BUNDLE_IDENTIFIER = com.matchlivetv.match-live-tv.tests;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
|
|||||||
Reference in New Issue
Block a user