Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
e3ae1ed
Update .gitignore for E2E reports, example app artifacts, and tooling
al-af May 12, 2026
87f0af2
Add Claude Code project config and rules
al-af May 12, 2026
f8dfe09
Replace legacy workflows with RC pipeline and E2E CI
al-af May 12, 2026
73ee38d
Add E2E scenario runner and test plans for iOS and Android
al-af May 12, 2026
28fc7aa
Add QA test app for E2E validation with auto-run flow
al-af May 12, 2026
b4d49ed
Add getSDKVersion bridge method and startSdk API
al-af May 12, 2026
d4505f7
Add RC release operator manual
al-af May 12, 2026
5dac047
fix: CI workflow correctness fixes for RC pipeline
al-af May 12, 2026
296eec9
ci: Remove npm cache from GitHub Actions workflows
al-af May 12, 2026
9722b2a
ci: Update dependency and Gradle build steps in workflows
al-af May 12, 2026
e8ad490
chore: Exclude example directory from ESLint
al-af May 12, 2026
e8569fd
ci: Refine automated release and smoke test workflows
al-af May 12, 2026
b1f725a
ci: Commit Gradle wrapper scripts for consistent builds
al-af May 13, 2026
2bd0f47
ci: Allow concurrent GitHub Actions workflow runs on same branch
al-af May 13, 2026
7beae98
ci: Enhance RC release workflow to handle skipped jobs
al-af May 13, 2026
0e0576c
ci: Enhance E2E test reliability and CI/CD stability
al-af May 13, 2026
1065dfb
ci: Improve CI build consistency and test log retrieval
al-af May 13, 2026
e80e52f
ci: Enhance NPM publishing security with OIDC
al-af May 13, 2026
4135920
ci: Enhance React Native E2E test plan for comprehensive coverage
al-af May 13, 2026
36ce403
Remove getCustomerUserId API from React Native plugin
al-af May 13, 2026
c9836c1
ci: Ensure accurate fresh-install attribution for scenario testing
al-af May 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
383 changes: 383 additions & 0 deletions .af-e2e/test-plan.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,383 @@
{
"_meta": {
"plan_id": "reactnative-e2e",
"plugin": "reactnative",
"version": "1.1.0",
"description": "Pre-publish E2E test plan for the AppsFlyer React Native plugin. Runs against plugin source in example/. Covers six scenarios: cold launch, background deep link, foreground deep link, custom events, identity APIs, and consent/stop. Mapped to E2E-001..E2E-006 in appsflyer-mobile-plugin-tooling/contracts/e2e-test-contract.md.",
"platforms": ["android", "ios"],
"schema_version": "1.0.0",
"tooling_contract_ref": "E2E-001, E2E-002, E2E-003, E2E-004, E2E-005, E2E-006"
},

"config": {
"android": {
"package_name": "com.appsflyer.engagement",
"activity": "com.appsflyer.qa.reactnative.MainActivity",
"apk_path": "example/android/app/build/outputs/apk/debug/app-debug.apk",
"build_cmd": "cd example/android && ./gradlew assembleDebug"
},
"ios": {
"bundle_id": "com.appsflyer.qa.reactnative",
"app_path": "example/ios/build/Build/Products/Debug-iphonesimulator/example.app",
"build_cmd": "cd example/ios && xcodebuild build -workspace example.xcworkspace -scheme example -configuration Debug -destination 'platform=iOS Simulator,id=$IOS_SIMULATOR_UDID' -derivedDataPath build"
}
},

"phases": [
{
"id": "phase_1",
"name": "Cold launch coverage",
"scenario_ref": "E2E-001",
"description": "Fresh install. Validate SDK startup, pre/post-start APIs, three auto-launched events with HTTP 200, install conversion data with is_first_launch=true, and onDeepLinking NOT_FOUND on clean launch.",
"requires_fresh_install": true,
"wait_after_launch_sec": 420,
"checks": [
{
"id": "sdk_started",
"description": "startSDK was called",
"type": "log_contains",
"pattern": "[AF_QA][startSDK] result:",
"fail_action": "abort"
},
{
"id": "is_first_launch_true",
"description": "onInstallConversionData fires with is_first_launch=true",
"type": "log_contains",
"pattern": "[AF_QA][CALLBACK][onInstallConversionData]",
"payload_check": {"field": "is_first_launch", "expected": "true"},
"fail_action": "abort"
},
{
"id": "pre_start_apis_complete",
"description": "Pre-start auto APIs ran",
"type": "log_contains",
"pattern": "[AF_QA][AUTO_APIS] --- Pre-start auto APIs complete ---",
"fail_action": "fail"
},
{
"id": "post_start_apis_complete",
"description": "Post-start auto APIs ran",
"type": "log_contains",
"pattern": "[AF_QA][AUTO_APIS] --- Post-start auto APIs complete ---",
"fail_action": "fail"
},
{
"id": "get_sdk_version",
"description": "getSDKVersion returns a value",
"type": "log_contains",
"pattern": "[AF_QA][getSDKVersion] result:",
"fail_action": "fail"
},
{
"id": "get_appsflyer_uid",
"description": "getAppsFlyerUID returns a value",
"type": "log_contains",
"pattern": "[AF_QA][getAppsFlyerUID] result:",
"fail_action": "fail"
},
{
"id": "event_af_demo_launch",
"description": "af_demo_launch event fires successfully",
"type": "log_contains",
"pattern": "[AF_QA][logEvent(af_demo_launch)] result:",
"fail_action": "fail"
},
{
"id": "event_af_purchase",
"description": "af_purchase event fires successfully",
"type": "log_contains",
"pattern": "[AF_QA][logEvent(af_purchase)] result:",
"fail_action": "fail"
},
{
"id": "event_af_content_view",
"description": "af_content_view event fires successfully",
"type": "log_contains",
"pattern": "[AF_QA][logEvent(af_content_view)] result:",
"fail_action": "fail"
},
{
"id": "http_200_count",
"description": "At least 3 HTTP 200 responses from AppsFlyer servers",
"type": "count_matches",
"pattern": "response code:200 OK|response_status=200",
"minimum": 3,
"fail_action": "fail"
},
{
"id": "on_deep_linking_callback",
"description": "onDeepLinking fires (NOT_FOUND expected on clean launch)",
"type": "log_contains",
"pattern": "[AF_QA][CALLBACK][onDeepLinking]",
"fail_action": "fail"
},
{
"id": "no_fatal_errors",
"description": "No fatal exceptions or SDK errors in logs",
"type": "absent",
"patterns": ["Fatal Exception", "FATAL", "[AF_QA][startSDK] error:", "response code:4", "response code:5"],
"fail_action": "fail"
}
]
},

{
"id": "phase_2",
"name": "Background deep link",
"scenario_ref": "E2E-002",
"description": "App backgrounded after Phase 1, then deep link triggers re-entry. onDeepLinking fires with Status.FOUND and correct deepLinkValue. LAUNCH event receives HTTP 200.",
"requires_fresh_install": false,
"wait_after_trigger_sec": 15,
"deep_link_url": "afqa-reactnative://deeplink?deep_link_value=qa_deeplink_bg&af_sub1=background_test&pid=testmedia&c=deeplink_test",
"pre_actions": {
"android": ["adb shell input keyevent KEYCODE_HOME", "sleep 2"],
"ios": ["xcrun simctl launch {{UDID}} com.apple.mobilesafari", "sleep 2"]
},
"trigger": {
"android": "adb shell am start -W -a android.intent.action.VIEW -d \"{{DEEP_LINK_URL}}\"",
"ios": "xcrun simctl openurl {{UDID}} \"{{DEEP_LINK_URL}}\""
},
"checks": [
{
"id": "deeplink_status_found",
"description": "onDeepLinking fires with Status.FOUND",
"type": "log_contains",
"pattern": "status=FOUND",
"fail_action": "fail"
},
{
"id": "deeplink_value_bg",
"description": "deepLinkValue matches qa_deeplink_bg",
"type": "log_contains",
"pattern": "deepLinkValue=qa_deeplink_bg",
"fail_action": "fail"
},
{
"id": "launch_http_200",
"description": "LAUNCH event receives HTTP 200 after deep link re-entry",
"type": "count_matches",
"pattern": "response code:200 OK|response_status=200",
"minimum": 1,
"fail_action": "fail"
},
{
"id": "no_fatal_errors",
"description": "No fatal exceptions after deep link",
"type": "absent",
"patterns": ["Fatal Exception", "FATAL"],
"fail_action": "fail"
}
]
},

{
"id": "phase_3",
"name": "Foreground deep link",
"scenario_ref": "E2E-003",
"description": "Fresh install. App in foreground after SDK start. Brief launcher switch, then deep link. onDeepLinking fires with Status.FOUND and correct deepLinkValue. Conversion data has is_first_launch=true.",
"requires_fresh_install": true,
"wait_after_launch_sec": 420,
"wait_after_trigger_sec": 15,
"deep_link_url": "afqa-reactnative://deeplink?deep_link_value=qa_deeplink_fg&af_sub1=foreground_test&pid=testmedia&c=deeplink_test",
"pre_actions": {
"android": ["adb shell am start -a android.intent.action.MAIN -c android.intent.category.HOME", "sleep 1"],
"ios": ["xcrun simctl launch {{UDID}} com.apple.Preferences", "sleep 1"]
},
"trigger": {
"android": "adb shell am start -W -a android.intent.action.VIEW -d \"{{DEEP_LINK_URL}}\"",
"ios": "xcrun simctl openurl {{UDID}} \"{{DEEP_LINK_URL}}\""
},
"checks": [
{
"id": "sdk_started",
"description": "startSDK was called on fresh install",
"type": "log_contains",
"pattern": "[AF_QA][startSDK] result:",
"fail_action": "abort"
},
{
"id": "is_first_launch_true",
"description": "onInstallConversionData fires with is_first_launch=true",
"type": "log_contains",
"pattern": "[AF_QA][CALLBACK][onInstallConversionData]",
"payload_check": {"field": "is_first_launch", "expected": "true"},
"fail_action": "abort"
},
{
"id": "deeplink_status_found",
"description": "onDeepLinking fires with Status.FOUND after foreground deep link",
"type": "log_contains",
"pattern": "status=FOUND",
"fail_action": "fail"
},
{
"id": "deeplink_value_fg",
"description": "deepLinkValue matches qa_deeplink_fg",
"type": "log_contains",
"pattern": "deepLinkValue=qa_deeplink_fg",
"fail_action": "fail"
},
{
"id": "no_fatal_errors",
"description": "No fatal exceptions",
"type": "absent",
"patterns": ["Fatal Exception", "FATAL"],
"fail_action": "fail"
}
]
},

{
"id": "phase_4",
"name": "Custom in-app event with parameters",
"scenario_ref": "E2E-004",
"description": "Trigger af_qa_custom_purchase with rich parameters (string, number, bool, nested map). Verify all parameter keys are serialized, HTTP 200 returned, and no logEvent errors.",
"requires_fresh_install": false,
"wait_after_launch_sec": 420,
"checks": [
{
"id": "custom_event_logged",
"description": "af_qa_custom_purchase logEvent fires",
"type": "log_contains",
"pattern": "[AF_QA][logEvent(af_qa_custom_purchase)]",
"fail_action": "fail"
},
{
"id": "params_complete",
"description": "All 5 parameter keys present in the custom event log line",
"type": "log_contains",
"pattern": "[AF_QA][logEvent] name=af_qa_custom_purchase params=",
"fail_action": "fail"
},
{
"id": "http_200_event",
"description": "HTTP 200 for the custom event",
"type": "count_matches",
"pattern": "response code:200 OK|response_status=200",
"minimum": 1,
"fail_action": "fail"
},
{
"id": "no_logEvent_error",
"description": "No logEvent errors for custom purchase",
"type": "absent",
"patterns": ["[AF_QA][logEvent(af_qa_custom_purchase)] error:"],
"fail_action": "fail"
},
{
"id": "no_fatal_errors",
"description": "No fatal exceptions or process crashes",
"type": "absent",
"patterns": ["Fatal Exception", "FATAL"],
"fail_action": "fail"
}
]
},

{
"id": "phase_5",
"name": "Identity APIs round-trip",
"scenario_ref": "E2E-005",
"description": "Fresh install. Verify setCustomerUserId, setCurrencyCode, setAdditionalData propagate correctly. Identity-check event receives HTTP 200. is_first_launch=true still fires.",
"requires_fresh_install": true,
"wait_after_launch_sec": 420,
"checks": [
{
"id": "customer_user_id_set",
"description": "setCustomerUserId readback present",
"type": "log_contains",
"pattern": "[AF_QA][setCustomerUserId] result:",
"fail_action": "fail"
},
{
"id": "currency_code",
"description": "setCurrencyCode readback present",
"type": "log_contains",
"pattern": "[AF_QA][setCurrencyCode] result:",
"fail_action": "fail"
},
{
"id": "additional_data",
"description": "setAdditionalData readback present",
"type": "log_contains",
"pattern": "[AF_QA][setAdditionalData] result:",
"fail_action": "fail"
},
{
"id": "http_200_identity_event",
"description": "HTTP 200 for the identity-check event",
"type": "count_matches",
"pattern": "response code:200 OK|response_status=200",
"minimum": 1,
"fail_action": "fail"
},
{
"id": "is_first_launch_true",
"description": "onInstallConversionData still fires with is_first_launch=true",
"type": "log_contains",
"pattern": "[AF_QA][CALLBACK][onInstallConversionData]",
"payload_check": {"field": "is_first_launch", "expected": "true"},
"fail_action": "fail"
},
{
"id": "no_fatal_errors",
"description": "No fatal exceptions or process crashes",
"type": "absent",
"patterns": ["Fatal Exception", "FATAL"],
"fail_action": "fail"
}
]
},

{
"id": "phase_6",
"name": "Consent / SDK stop toggle",
"scenario_ref": "E2E-006",
"description": "stop(true) suppresses outbound events. stop(false) resumes them. Verify suppressed event does NOT get HTTP 200, resumed event DOES get HTTP 200.",
"requires_fresh_install": false,
"wait_after_launch_sec": 420,
"checks": [
{
"id": "stop_true",
"description": "stop(true) readback present",
"type": "log_contains",
"pattern": "[AF_QA][stop] result: true",
"fail_action": "fail"
},
{
"id": "suppressed_event_no_result",
"description": "Suppressed event does not get a success result while SDK is stopped",
"type": "absent",
"patterns": ["[AF_QA][logEvent(af_qa_suppressed)] result:"],
"fail_action": "fail"
},
{
"id": "stop_false",
"description": "stop(false) readback present",
"type": "log_contains",
"pattern": "[AF_QA][stop] result: false",
"fail_action": "fail"
},
{
"id": "resumed_event_http_200",
"description": "Resumed event fires and receives HTTP 200",
"type": "log_contains",
"pattern": "[AF_QA][logEvent(af_qa_resumed)] result:",
"fail_action": "fail"
},
{
"id": "no_fatal_errors",
"description": "No fatal exceptions throughout stop/resume cycle",
"type": "absent",
"patterns": ["Fatal Exception", "FATAL"],
"fail_action": "fail"
}
]
}
],

"report": {
"output_dir": ".af-e2e/reports",
"format": "json"
}
}
Loading
Loading