mirror of
https://github.com/RevenueCat/purchases-ios
synced 2026-11-09 22:35:56 +05:45
Automated E2E Tests on all backend environments (#5712)
* Testing maestro e2e * Testing e2e * Testing e2e * Testing e2e * Testing e2e * Testing e2e * Testing e2e * Implemented Fastlane action for installing the right StoreKit config file to the simulator that will be used for maestro testing * Made the setup simulator storekit config action a lot more stable by updating the code finding the storekit shared app group container * Added script for generating CI.xcconfig file to be used by the CI runs * Updated config.yml * Updated maestro flows path * Made the e2e test lane reusable with a test_environment argument, added storekit config file for load shedder. Setup CircleCI job for all 3 test_environments * Fix passing load shedder api key to command * Updated storing of test output * Added test environment name to the e2e test runs * Restore missing install_dependencies call * Also store test output as artifacts * Allow setting force_server_error_strategy from env variable, using it in the fallback e2e tests. * Using CircleCI matrix for test_environments * Added support for showing different E2E flows in the app depending on which maestro flow is run to avoid tests breaking if changes are made * Using new automated sdk project. Using a single test flow and the environment is now soley based on API key and server error strategy environment variables * Updated context and environment variables for e2e tests * Only generate maestro workspace * Test * Fixed incorrect env variables being used * Fix formatting * Simplified env var -> xcconfig logic to avoid using wrong values * Fix config * Updated maestro screenshot names * Fixed forceServerErrorStrategy * Reusing maestro e2e test all environments * Added fortress 2 as load_shedder_secondary test environment * Rename test_environment to backend_environment * use default value for force_server_error_strategy * Updated flow names * Added daily e2e test schedule * Removed unused parameter * Revert to original build and test step after testing * Passing the api_host to the maestro test app for the load shedder backends, using the normal API key * Renamed load_shedder_primary and load_shedder_secondary * Renamed forceServerErrorStrategy case and removed protocl from api_host * Small code changes * Rename parameter target to query to align with Tuist terminology * Small code changes to address PR feedback
This commit is contained in:
18 files changed
+579
-11
No files matched your search
+131
-2
@@ -1,7 +1,7 @@
|
||||
orbs:
|
||||
macos: circleci/macos@2.5.1
|
||||
slack: circleci/slack@5.0.0
|
||||
revenuecat: revenuecat/sdks-common-config@3.2.0
|
||||
revenuecat: revenuecat/sdks-common-config@3.9.0
|
||||
# Disabled until compatible with M1: codecov: codecov/codecov@3.3.0
|
||||
# codecov: codecov/codecov@3.3.0
|
||||
|
||||
@@ -116,6 +116,15 @@ aliases:
|
||||
ignore: /.*/
|
||||
branches:
|
||||
only: main
|
||||
maestro-e2e-test-job: &maestro-e2e-test-job
|
||||
run-maestro-e2e-tests:
|
||||
name: run-maestro-e2e-tests-<< matrix.backend_environment >>
|
||||
context:
|
||||
- e2e-tests
|
||||
- slack-secrets-ios
|
||||
matrix:
|
||||
parameters:
|
||||
backend_environment: ["production", "load_shedder_us_east_1", "load_shedder_us_east_2", "fallback"]
|
||||
|
||||
commands:
|
||||
slack-notify-on-fail:
|
||||
@@ -413,13 +422,22 @@ commands:
|
||||
|
||||
tuist-generate-workspace:
|
||||
description: "Install Tuist and generate workspace"
|
||||
parameters:
|
||||
query:
|
||||
type: string
|
||||
default: ""
|
||||
description: "Optional query to pass to Tuist when generating workspace."
|
||||
steps:
|
||||
- revenuecat/install-tuist
|
||||
- run:
|
||||
name: Generate Tuist workspace
|
||||
command: |
|
||||
export PATH="$HOME/.local/share/mise/shims:$PATH"
|
||||
tuist install && tuist generate --no-open
|
||||
if [ -n "<< parameters.query >>" ]; then
|
||||
tuist install && tuist generate << parameters.query >> --no-open
|
||||
else
|
||||
tuist install && tuist generate --no-open
|
||||
fi
|
||||
|
||||
create-snapshot-pr-if-needed:
|
||||
parameters:
|
||||
@@ -480,6 +498,50 @@ commands:
|
||||
- revenuecat/set-ssh-key-for-git:
|
||||
fingerprint: "SHA256:UYi3gsOInLRPZy6MPho7Yr/YK1VmjDvPGo7sJvIcnPM"
|
||||
|
||||
setup_xcconfig:
|
||||
parameters:
|
||||
api_key:
|
||||
type: string
|
||||
description: "The RevenueCat API key value"
|
||||
force_server_error_strategy:
|
||||
type: enum
|
||||
enum: ["never", "primary_backend_down"]
|
||||
default: "never"
|
||||
description: "Server error strategy for testing fallback behavior"
|
||||
api_host:
|
||||
type: string
|
||||
default: ""
|
||||
description: "Optional API host for testing (e.g., 'api-staging.revenuecat.com')"
|
||||
proxy_url_scheme:
|
||||
type: string
|
||||
default: ""
|
||||
description: "Optional proxy URL scheme (e.g., 'https')"
|
||||
proxy_url_host:
|
||||
type: string
|
||||
default: ""
|
||||
description: "Optional proxy URL host (e.g., 'proxy.revenuecat.com')"
|
||||
steps:
|
||||
- run:
|
||||
name: Create XCConfig file
|
||||
command: |
|
||||
{
|
||||
echo "// Auto-generated. Do not commit to VCS"
|
||||
echo "// Generated on $(date)"
|
||||
echo ""
|
||||
echo "REVENUECAT_API_KEY = << parameters.api_key >>"
|
||||
echo "REVENUECAT_FORCE_SERVER_ERROR_STRATEGY = << parameters.force_server_error_strategy >>"
|
||||
if [ -n "<< parameters.api_host >>" ]; then
|
||||
echo "REVENUECAT_API_HOST = << parameters.api_host >>"
|
||||
fi
|
||||
if [ -n "<< parameters.proxy_url_scheme >>" ]; then
|
||||
echo "REVENUECAT_PROXY_URL_SCHEME = << parameters.proxy_url_scheme >>"
|
||||
fi
|
||||
if [ -n "<< parameters.proxy_url_host >>" ]; then
|
||||
echo "REVENUECAT_PROXY_URL_HOST = << parameters.proxy_url_host >>"
|
||||
fi
|
||||
} > CI.xcconfig
|
||||
echo "✅ Created CI.xcconfig"
|
||||
|
||||
jobs:
|
||||
validate-package-swift:
|
||||
parameters:
|
||||
@@ -1135,6 +1197,64 @@ jobs:
|
||||
test_plan: "BackendIntegrationTests-CustomEntitlements"
|
||||
- slack-notify-on-fail
|
||||
|
||||
run-maestro-e2e-tests:
|
||||
parameters:
|
||||
backend_environment:
|
||||
type: enum
|
||||
enum: ["production", "load_shedder_us_east_1", "load_shedder_us_east_2", "fallback"]
|
||||
executor:
|
||||
name: macos-executor
|
||||
xcode_version: "26.0.1"
|
||||
steps:
|
||||
- macos/preboot-simulator:
|
||||
version: "18.6"
|
||||
platform: "iOS"
|
||||
device: "iPhone 16 Pro"
|
||||
- checkout
|
||||
- install-dependencies:
|
||||
install_swiftlint: false
|
||||
- when:
|
||||
condition:
|
||||
equal: [ "load_shedder_us_east_1", << parameters.backend_environment >> ]
|
||||
steps:
|
||||
- setup_xcconfig:
|
||||
api_key: ${RC_E2E_TEST_API_KEY_PRODUCTION_APP_STORE}
|
||||
api_host: fortress-us-east-1.revenuecat.com
|
||||
- when:
|
||||
condition:
|
||||
equal: [ "load_shedder_us_east_2", << parameters.backend_environment >> ]
|
||||
steps:
|
||||
- setup_xcconfig:
|
||||
api_key: ${RC_E2E_TEST_API_KEY_PRODUCTION_APP_STORE}
|
||||
api_host: fortress-us-east-2.revenuecat.com
|
||||
- when:
|
||||
condition:
|
||||
equal: [ "fallback", << parameters.backend_environment >> ]
|
||||
steps:
|
||||
- setup_xcconfig:
|
||||
api_key: ${RC_E2E_TEST_API_KEY_PRODUCTION_APP_STORE}
|
||||
force_server_error_strategy: primary_backend_down
|
||||
- when:
|
||||
condition:
|
||||
equal: [ "production", << parameters.backend_environment >> ]
|
||||
steps:
|
||||
- setup_xcconfig:
|
||||
api_key: ${RC_E2E_TEST_API_KEY_PRODUCTION_APP_STORE}
|
||||
- revenuecat/install-maestro
|
||||
- tuist-generate-workspace:
|
||||
query: "Maestro"
|
||||
- run:
|
||||
name: Run e2e tests (<< parameters.backend_environment >>)
|
||||
command: bundle exec fastlane run_maestro_e2e_tests
|
||||
no_output_timeout: 15m
|
||||
|
||||
# todo: implement error / success notifications through Slack
|
||||
|
||||
- store_test_results:
|
||||
path: fastlane/test_output
|
||||
- store_artifacts:
|
||||
path: fastlane/test_output
|
||||
|
||||
release-checks:
|
||||
executor:
|
||||
name: macos-executor
|
||||
@@ -1663,6 +1783,14 @@ workflows:
|
||||
- slack-secrets
|
||||
- integration-tests-all
|
||||
|
||||
daily-maestro-e2e-tests:
|
||||
when:
|
||||
and:
|
||||
- equal: [scheduled_pipeline, << pipeline.trigger_source >>]
|
||||
- equal: ["maestro_e2e_tests", << pipeline.schedule.name >>]
|
||||
jobs:
|
||||
- <<: *maestro-e2e-test-job
|
||||
|
||||
manual-trigger-bump:
|
||||
when:
|
||||
equal: [bump, << pipeline.parameters.action >>]
|
||||
@@ -1712,6 +1840,7 @@ workflows:
|
||||
- equal:
|
||||
["run-from-github-comments", << pipeline.parameters.GHA_Meta >>]
|
||||
jobs:
|
||||
- <<: *maestro-e2e-test-job
|
||||
- backend-integration-tests-SK1:
|
||||
context:
|
||||
- slack-secrets-ios
|
||||
|
||||
@@ -66,6 +66,9 @@ fastlane/.env
|
||||
# Local build settings
|
||||
Local.xcconfig
|
||||
|
||||
# CI Build settings
|
||||
CI.xcconfig
|
||||
|
||||
# Gets created when Cursor is used with the sswg Swift extension
|
||||
/.index-build/
|
||||
|
||||
|
||||
@@ -21,7 +21,9 @@ let project = Project(
|
||||
],
|
||||
"REVENUECAT_API_KEY": "$(REVENUECAT_API_KEY)",
|
||||
"REVENUECAT_PROXY_URL_SCHEME": "$(REVENUECAT_PROXY_URL_SCHEME)",
|
||||
"REVENUECAT_PROXY_URL_HOST": "$(REVENUECAT_PROXY_URL_HOST)"
|
||||
"REVENUECAT_PROXY_URL_HOST": "$(REVENUECAT_PROXY_URL_HOST)",
|
||||
"REVENUECAT_FORCE_SERVER_ERROR_STRATEGY": "$(REVENUECAT_FORCE_SERVER_ERROR_STRATEGY)",
|
||||
"REVENUECAT_API_HOST": "$(REVENUECAT_API_HOST)"
|
||||
]
|
||||
),
|
||||
sources: ["rc-maestro/Sources/**/*.swift"],
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
appId: com.revenuecat.maestro.ios
|
||||
name: Subscribe to product through V1 Paywall for alternative offering
|
||||
|
||||
---
|
||||
- launchApp:
|
||||
arguments:
|
||||
e2e_test_flow: subscribe_from_v1_paywall
|
||||
- tapOn:
|
||||
text: "Present Paywall"
|
||||
- assertVisible: "Paywall V1"
|
||||
- takeScreenshot: "subscribe_from_v1_paywall - Paywall"
|
||||
- tapOn:
|
||||
text: "Monthly"
|
||||
- tapOn:
|
||||
text: "Continue"
|
||||
- takeScreenshot: "subscribe_from_v1_paywall - IAP Sheet"
|
||||
- tapOn:
|
||||
text: "Subscribe"
|
||||
- takeScreenshot: "subscribe_from_v1_paywall- IAP Confirmation"
|
||||
- tapOn:
|
||||
text: "OK"
|
||||
@@ -0,0 +1,21 @@
|
||||
appId: com.revenuecat.maestro.ios
|
||||
name: Subscribe to product through V2 Paywall for default offering
|
||||
|
||||
---
|
||||
- launchApp:
|
||||
arguments:
|
||||
e2e_test_flow: subscribe_from_v2_paywall
|
||||
- tapOn:
|
||||
text: "Present Paywall"
|
||||
- assertVisible: "Paywall V2"
|
||||
- takeScreenshot: "subscribe_from_v2_paywall - Paywall"
|
||||
- tapOn:
|
||||
text: "Yearly"
|
||||
- tapOn:
|
||||
text: "Continue"
|
||||
- takeScreenshot: "subscribe_from_v2_paywall - IAP Sheet"
|
||||
- tapOn:
|
||||
text: "Subscribe"
|
||||
- takeScreenshot: "subscribe_from_v2_paywall - IAP Confirmation"
|
||||
- tapOn:
|
||||
text: "OK"
|
||||
@@ -0,0 +1 @@
|
||||
{"appPolicies":{"eula":"","policies":[{"locale":"en_US","policyText":"","policyURL":""}]},"identifier":"517FAB12","nonRenewingSubscriptions":[],"products":[],"subscriptionGroups":[{"id":"3723925D","localizations":[],"name":"Default offering","subscriptions":[{"adHocOffers":[],"codeOffers":[],"displayPrice":"9.99","familyShareable":false,"groupNumber":1,"internalID":"3F6716CC","localizations":[{"description":"","displayName":"","locale":"en_US"}],"productID":"pro_monthly_subscription","recurringSubscriptionPeriod":"P1M","referenceName":"Pro monthly","subscriptionGroupID":"3723925D","type":"RecurringSubscription","winbackOffers":[]},{"adHocOffers":[],"codeOffers":[],"displayPrice":"79.99","familyShareable":false,"groupNumber":1,"internalID":"42EDF3B1","localizations":[{"description":"","displayName":"","locale":"en_US"}],"productID":"pro_annual_subscription","recurringSubscriptionPeriod":"P1Y","referenceName":"Pro annually","subscriptionGroupID":"3723925D","type":"RecurringSubscription","winbackOffers":[]}]}],"version":{"major":4,"minor":0}}
|
||||
@@ -34,4 +34,36 @@ enum Constants {
|
||||
}
|
||||
return "\(scheme)//\(host)"
|
||||
}()
|
||||
|
||||
/*
|
||||
The force server error strategy to configure Purchases with
|
||||
To be used in (e2e) tests in order to simulate server failures
|
||||
REVENUECAT_FORCE_SERVER_ERROR_STRATEGY = primary_domain_down
|
||||
*/
|
||||
static var forceServerErrorStrategy: Constants.ForceServerErrorStrategy {
|
||||
guard let value = Bundle.main.object(forInfoDictionaryKey: "REVENUECAT_FORCE_SERVER_ERROR_STRATEGY") as? String else {
|
||||
return .never
|
||||
}
|
||||
|
||||
return ForceServerErrorStrategy(rawValue: value) ?? .never
|
||||
}
|
||||
|
||||
/*
|
||||
The API base URL to configure SystemInfo with
|
||||
To be used in (e2e) tests in order to test against different environments
|
||||
REVENUECAT_API_HOST = api-staging.revenuecat.com
|
||||
*/
|
||||
static var apiBaseURL: URL? {
|
||||
guard let host = Bundle.main.object(forInfoDictionaryKey: "REVENUECAT_API_HOST") as? String,
|
||||
!host.isEmpty,
|
||||
let url = URL(string: "https://" + host) else {
|
||||
return nil
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
||||
enum ForceServerErrorStrategy: String {
|
||||
case primaryBackendDown = "primary_backend_down"
|
||||
case never
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,7 @@ public struct ContentView: View {
|
||||
pushCustomerCenter = true
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.navigationDestination(isPresented: $pushCustomerCenter, destination: {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import SwiftUI
|
||||
import RevenueCat
|
||||
@testable import RevenueCat
|
||||
|
||||
@main
|
||||
struct RcMaestroApp: App {
|
||||
@@ -7,12 +7,68 @@ struct RcMaestroApp: App {
|
||||
init() {
|
||||
Purchases.logLevel = .verbose
|
||||
Purchases.proxyURL = Constants.proxyURL.flatMap { URL(string: $0) }
|
||||
Purchases.configure(withAPIKey: Constants.apiKey)
|
||||
|
||||
// Set API base URL if provided (used in E2E tests)
|
||||
if let apiBaseURL = Constants.apiBaseURL {
|
||||
SystemInfo.apiBaseURL = apiBaseURL
|
||||
}
|
||||
|
||||
// Used in E2E tests
|
||||
Purchases.configure(
|
||||
with: .builder(withAPIKey: Constants.apiKey)
|
||||
.with(dangerousSettings: .init(
|
||||
autoSyncPurchases: true,
|
||||
internalSettings: DangerousSettings.Internal(
|
||||
forceServerErrorStrategy: .init { request in
|
||||
switch Constants.forceServerErrorStrategy {
|
||||
case .never:
|
||||
return false
|
||||
case .primaryBackendDown:
|
||||
return request.fallbackUrlIndex == nil
|
||||
}
|
||||
}
|
||||
)
|
||||
))
|
||||
.build()
|
||||
)
|
||||
}
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
ContentView()
|
||||
switch e2eTestFlow {
|
||||
case .some(let flow):
|
||||
flow.view
|
||||
case nil:
|
||||
ContentView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Parses the launch argument with the e2e test flow to run
|
||||
*/
|
||||
fileprivate var e2eTestFlow: E2ETestFlow? {
|
||||
guard let string = UserDefaults.standard.dictionaryRepresentation()["e2e_test_flow"] as? String else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return E2ETestFlow(rawValue: string)
|
||||
}
|
||||
}
|
||||
|
||||
enum E2ETestFlow: String {
|
||||
case subscribeFromV1Paywall = "subscribe_from_v1_paywall"
|
||||
case subscribeFromV2Paywall = "subscribe_from_v2_paywall"
|
||||
|
||||
@ViewBuilder
|
||||
var view: some View {
|
||||
switch self {
|
||||
case .subscribeFromV1Paywall:
|
||||
E2ETestFlowView.SubscribeFromV1Paywall()
|
||||
case .subscribeFromV2Paywall:
|
||||
E2ETestFlowView.SubscribeFromV2Paywall()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum E2ETestFlowView {}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
//
|
||||
// subscribe-from-v1-paywall.swift
|
||||
// Maestro
|
||||
//
|
||||
// Created by Rick van der Linden on 30/10/2025.
|
||||
// Copyright © 2025 RevenueCat, Inc. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import RevenueCat
|
||||
import RevenueCatUI
|
||||
|
||||
extension E2ETestFlowView {
|
||||
struct SubscribeFromV1Paywall: View {
|
||||
|
||||
@State private var offering: Offering?
|
||||
@State private var presentPaywall = false
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Text("V1 Paywall - alternative offering")
|
||||
.font(.largeTitle)
|
||||
|
||||
if offering != nil {
|
||||
Button("Present Paywall") {
|
||||
presentPaywall = true
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
}
|
||||
else {
|
||||
Text("Loading offerings...")
|
||||
}
|
||||
}
|
||||
.task {
|
||||
let offerings = try? await Purchases.shared.offerings()
|
||||
self.offering = offerings?.offering(identifier: "paywall_v1")
|
||||
}
|
||||
.sheet(isPresented: $presentPaywall) {
|
||||
if let offering {
|
||||
PaywallView(offering: offering)
|
||||
}
|
||||
}
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
//
|
||||
// subscribe-from-v2-paywall.swift
|
||||
// Maestro
|
||||
//
|
||||
// Created by Rick van der Linden on 30/10/2025.
|
||||
// Copyright © 2025 RevenueCat, Inc. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import RevenueCat
|
||||
import RevenueCatUI
|
||||
|
||||
extension E2ETestFlowView {
|
||||
struct SubscribeFromV2Paywall: View {
|
||||
|
||||
@State private var presentPaywall = false
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Text("V2 Paywall - default offering")
|
||||
.font(.largeTitle)
|
||||
Button("Present Paywall") {
|
||||
presentPaywall = true
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
}
|
||||
.sheet(isPresented: $presentPaywall) {
|
||||
PaywallView()
|
||||
}
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,4 +24,5 @@
|
||||
// Note that this file is read by the RevenueCat, PurchaseTester and PaywallsTester projects.
|
||||
// ======
|
||||
|
||||
#include? "CI.xcconfig"
|
||||
#include? "Local.xcconfig"
|
||||
@@ -37,4 +37,8 @@
|
||||
// REVENUECAT_PROXY_URL_SCHEME = http
|
||||
// REVENUECAT_PROXY_URL_HOST = localhost:9000
|
||||
|
||||
// Because two slashes are interpreted as a comment in this file, the scheme and host need to be set separately
|
||||
// Because two slashes are interpreted as a comment in this file, the scheme and host need to be set separately
|
||||
|
||||
// The following configuration options can be used when running the E2E tests through the Tuist generated Maestro project
|
||||
// REVENUECAT_FORCE_SERVER_ERROR_STRATEGY = primary_backend_down / never
|
||||
// REVENUECAT_API_HOST = custom-api-host.revenuecat.com
|
||||
+11
-2
@@ -9,13 +9,22 @@ import PackageDescription
|
||||
/// flags defined in it. It does nothing if this file does not exist in this exact folder. This file does not exist on
|
||||
/// a clean checkout. It has to be created manually by a developer.
|
||||
var additionalCompilerFlags: [PackageDescription.SwiftSetting] = {
|
||||
guard let config = try? String(
|
||||
let ciConfig = try? String(
|
||||
contentsOf: URL(fileURLWithPath: #filePath)
|
||||
.deletingLastPathComponent()
|
||||
.appendingPathComponent("CI.xcconfig")
|
||||
)
|
||||
|
||||
let localConfig = try? String(
|
||||
contentsOf: URL(fileURLWithPath: #filePath)
|
||||
.deletingLastPathComponent()
|
||||
.appendingPathComponent("Local.xcconfig")
|
||||
) else {
|
||||
)
|
||||
|
||||
guard let config = ciConfig ?? localConfig else {
|
||||
return []
|
||||
}
|
||||
|
||||
// We split the capture group by space and remove any special flags, such as $(inherited).
|
||||
return config
|
||||
.firstMatch(of: #/^SWIFT_ACTIVE_COMPILATION_CONDITIONS *= *(.*)$/#.anchorsMatchLineEndings())?
|
||||
|
||||
+3
-1
@@ -27,7 +27,9 @@ var additionalFiles: [FileElement] = [
|
||||
.glob(pattern: "Tests/TestPlans/**/*.xctestplan"),
|
||||
.glob(pattern: "Tests/RevenueCatUITests/TestPlans/**/*.xctestplan")
|
||||
]
|
||||
if FileManager.default.fileExists(atPath: "Local.xcconfig") {
|
||||
if FileManager.default.fileExists(atPath: "CI.xcconfig") {
|
||||
additionalFiles.append(.glob(pattern: "CI.xcconfig"))
|
||||
} else if FileManager.default.fileExists(atPath: "Local.xcconfig") {
|
||||
additionalFiles.append(.glob(pattern: "Local.xcconfig"))
|
||||
}
|
||||
|
||||
|
||||
@@ -802,6 +802,56 @@ platform :ios do
|
||||
configuration: 'release',
|
||||
)
|
||||
end
|
||||
|
||||
desc "run_maestro_e2e_tests"
|
||||
lane :run_maestro_e2e_tests do |options|
|
||||
bundle_id = "com.revenuecat.maestro.ios"
|
||||
base_path = "#{Dir.pwd}/.."
|
||||
|
||||
UI.message("Running Maestro E2E tests")
|
||||
|
||||
storekit_config_path = "#{base_path}/Examples/rc-maestro/rc-maestro/Resources/StoreKit/app787ddb07e6.storekit"
|
||||
|
||||
# Verify StoreKit config exists
|
||||
unless File.exist?(storekit_config_path)
|
||||
UI.user_error!("StoreKit configuration not found: #{storekit_config_path}")
|
||||
end
|
||||
|
||||
# note: it's called "run" tests but these tests only need to build to run
|
||||
xcworkspace = 'RevenueCat-Tuist.xcworkspace'
|
||||
unless File.directory?(xcworkspace)
|
||||
UI.important("#{xcworkspace} not found. Run `tuist generate --no-open` before invoking this lane.")
|
||||
end
|
||||
|
||||
xcodebuild(
|
||||
workspace: xcworkspace,
|
||||
scheme: "Maestro",
|
||||
sdk: "iphonesimulator",
|
||||
configuration: "Debug",
|
||||
derivedDataPath: "./build"
|
||||
)
|
||||
|
||||
sh("xcrun", "simctl", "install", "booted", "#{base_path}/build/Build/Products/Debug-iphonesimulator/Maestro.app")
|
||||
|
||||
# Setup StoreKit configuration in simulator
|
||||
setup_simulator_storekit_config(
|
||||
bundle_id: bundle_id,
|
||||
storekit_config_path: storekit_config_path
|
||||
)
|
||||
|
||||
# Construct the flow directory path
|
||||
flow_dir = "#{base_path}/Examples/rc-maestro/maestro/e2e_tests"
|
||||
|
||||
# Verify the flow directory exists
|
||||
unless File.directory?(flow_dir)
|
||||
UI.user_error!("Flow directory not found: #{flow_dir}")
|
||||
end
|
||||
|
||||
UI.message("Using flow directory: #{flow_dir}")
|
||||
|
||||
sh("maestro", "test", "--format", "junit", "--output", "test_output/maestro/report.xml", "--test-output-dir", "test_output/maestro", flow_dir)
|
||||
|
||||
end
|
||||
|
||||
desc "replace API KEY for installation and integration tests"
|
||||
lane :replace_api_key_integration_tests do
|
||||
|
||||
@@ -218,6 +218,14 @@ archive all platforms
|
||||
|
||||
run API Tests
|
||||
|
||||
### ios run_maestro_e2e_tests
|
||||
|
||||
```sh
|
||||
[bundle exec] fastlane ios run_maestro_e2e_tests
|
||||
```
|
||||
|
||||
run_maestro_e2e_tests
|
||||
|
||||
### ios replace_api_key_integration_tests
|
||||
|
||||
```sh
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
module Fastlane
|
||||
module Actions
|
||||
class SetupSimulatorStorekitConfigAction < Action
|
||||
def self.run(params)
|
||||
bundle_id = params[:bundle_id]
|
||||
storekit_config_path = params[:storekit_config_path]
|
||||
|
||||
# Verify StoreKit config exists
|
||||
unless File.exist?(storekit_config_path)
|
||||
UI.user_error!("StoreKit configuration not found at: #{storekit_config_path}")
|
||||
end
|
||||
|
||||
UI.message("Setting up StoreKit configuration for app: #{bundle_id}")
|
||||
|
||||
# Get the app container path
|
||||
container_path = Actions.sh("xcrun", "simctl", "get_app_container", "booted", bundle_id, log: false).strip
|
||||
UI.message("App container path: #{container_path}")
|
||||
|
||||
# Extract the base path up to and including "data/Containers"
|
||||
unless container_path =~ /(.*\/data\/Containers)\//
|
||||
UI.user_error!("Could not extract base path from: #{container_path}")
|
||||
end
|
||||
|
||||
base_container_path = $1
|
||||
UI.message("Base container path: #{base_container_path}")
|
||||
|
||||
# Find the correct app group container by checking metadata plist
|
||||
app_group_pattern = "#{base_container_path}/Shared/AppGroup/*"
|
||||
UI.message("Searching for app group containers: #{app_group_pattern}")
|
||||
|
||||
app_group_dirs = Dir.glob(app_group_pattern)
|
||||
|
||||
if app_group_dirs.empty?
|
||||
UI.user_error!("No app group containers found at: #{app_group_pattern}")
|
||||
end
|
||||
|
||||
# Find the app group with MCMMetadataIdentifier = group.com.apple.storekit
|
||||
correct_app_group = app_group_dirs.find do |app_group_dir|
|
||||
metadata_plist = File.join(app_group_dir, ".com.apple.mobile_container_manager.metadata.plist")
|
||||
next false unless File.exist?(metadata_plist)
|
||||
|
||||
begin
|
||||
# Use plutil to extract the MCMMetadataIdentifier value directly
|
||||
identifier = Actions.sh("plutil", "-extract", "MCMMetadataIdentifier", "raw", "-o", "-", metadata_plist, log: false).strip
|
||||
identifier == "group.com.apple.storekit"
|
||||
rescue => e
|
||||
UI.verbose("Failed to read plist at #{metadata_plist}: #{e.message}")
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
if correct_app_group.nil?
|
||||
UI.user_error!("Could not find StoreKit app group container (MCMMetadataIdentifier == group.com.apple.storekit)")
|
||||
end
|
||||
|
||||
UI.success("Found StoreKit app group container: #{correct_app_group}")
|
||||
|
||||
# Create the Octane folder if it doesn't exist
|
||||
octane_path = File.join(correct_app_group, "Documents", "Persistence", "Octane")
|
||||
|
||||
unless File.directory?(octane_path)
|
||||
UI.message("Octane folder doesn't exist, creating it...")
|
||||
Actions.sh("mkdir", "-p", octane_path, log: false)
|
||||
end
|
||||
|
||||
UI.success("Octane path: #{octane_path}")
|
||||
|
||||
# Construct the destination paths
|
||||
storekit_dir = File.join(octane_path, bundle_id, "Configuration.storekit")
|
||||
storekit_file = File.join(storekit_dir, "Configuration.storekit")
|
||||
|
||||
UI.message("Creating directory: #{storekit_dir}")
|
||||
Actions.sh("mkdir", "-p", storekit_dir, log: false)
|
||||
|
||||
UI.message("Copying StoreKit configuration...")
|
||||
Actions.sh("cp", storekit_config_path, storekit_file, log: false)
|
||||
|
||||
# Verify
|
||||
unless File.exist?(storekit_file)
|
||||
UI.user_error!("Failed to copy StoreKit configuration")
|
||||
end
|
||||
|
||||
UI.success("✅ StoreKit configuration installed at:")
|
||||
UI.message(" #{storekit_file}")
|
||||
|
||||
storekit_file
|
||||
end
|
||||
|
||||
def self.description
|
||||
"Installs a StoreKit configuration file in the correct location for a simulator app to use during testing"
|
||||
end
|
||||
|
||||
def self.details
|
||||
"This action copies a StoreKit configuration file to the Octane directory structure " \
|
||||
"within the iOS simulator, allowing the app to use local StoreKit testing without " \
|
||||
"connecting to the App Store. The action finds the correct app group container by " \
|
||||
"reading .com.apple.mobile_container_manager.metadata.plist and checking for " \
|
||||
"MCMMetadataIdentifier == 'group.com.apple.storekit', creates the Documents/Persistence/Octane " \
|
||||
"folder if needed, and places the configuration file at: " \
|
||||
"data/Containers/Shared/AppGroup/{UUID}/Documents/Persistence/Octane/{bundle_id}/Configuration.storekit/Configuration.storekit"
|
||||
end
|
||||
|
||||
def self.available_options
|
||||
[
|
||||
FastlaneCore::ConfigItem.new(
|
||||
key: :bundle_id,
|
||||
description: "The bundle identifier of the app installed in the simulator",
|
||||
type: String,
|
||||
optional: false
|
||||
),
|
||||
FastlaneCore::ConfigItem.new(
|
||||
key: :storekit_config_path,
|
||||
description: "Path to the StoreKit configuration file (.storekit)",
|
||||
type: String,
|
||||
optional: false,
|
||||
verify_block: proc do |value|
|
||||
UI.user_error!("StoreKit config file not found at '#{value}'") unless File.exist?(value)
|
||||
UI.user_error!("File must be a .storekit file") unless value.end_with?('.storekit')
|
||||
end
|
||||
)
|
||||
]
|
||||
end
|
||||
|
||||
def self.return_value
|
||||
"The full path to the installed StoreKit configuration file"
|
||||
end
|
||||
|
||||
def self.authors
|
||||
["rickvdl"]
|
||||
end
|
||||
|
||||
def self.is_supported?(platform)
|
||||
[:ios].include?(platform)
|
||||
end
|
||||
|
||||
def self.example_code
|
||||
[
|
||||
'setup_simulator_storekit_config(
|
||||
bundle_id: "com.example.myapp",
|
||||
storekit_config_path: "path/to/StoreKitConfiguration.storekit"
|
||||
)'
|
||||
]
|
||||
end
|
||||
|
||||
def self.category
|
||||
:testing
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in new issue
Block a user