Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
116 changes: 116 additions & 0 deletions .github/workflows/ui-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,86 @@ jobs:
BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }}
working-directory: sample-unity6/Tests
run: pytest -xs test/test_mac.py::MacTest
- name: Collect Unity Player logs (macOS)
if: always()
run: |
set -euo pipefail
workspaceLogs="${{ github.workspace }}/sample-unity6/player-logs"
zipPath="${{ github.workspace }}/sample-unity6/Unity6-macOS-PlayerLogs.zip"
mkdir -p "$workspaceLogs"

# Unity Editor logs (useful for build/package debugging)
unityLogsDir="$HOME/Library/Logs/Unity"
if [ -d "$unityLogsDir" ]; then
cp -R "$unityLogsDir" "$workspaceLogs/UnityLogs" || true
echo "Copied Unity logs from: $unityLogsDir"
else
echo "Unity logs directory not found: $unityLogsDir"
fi

# Unity Player logs (the standalone app's Player.log). This is what we actually
# need when UI tests fail at runtime. On macOS it's typically under:
# ~/Library/Logs/<CompanyName>/<ProductName>/Player.log
python3 - <<'PY'
import os
import shutil
from pathlib import Path

home = Path.home()
logs_root = home / "Library" / "Logs"
workspace = Path(os.environ.get("GITHUB_WORKSPACE", "."))
dest_dir = workspace / "sample-unity6" / "player-logs" / "PlayerLogs"
dest_dir.mkdir(parents=True, exist_ok=True)

candidates = []
if logs_root.exists():
# Prefer company folders we expect, but fall back to scanning all Logs.
preferred_roots = [
logs_root / "Immutable",
logs_root / "unity3d",
logs_root,
]
for root in preferred_roots:
if root.exists():
candidates.extend(root.rglob("Player.log"))

copied = 0
for plog in candidates:
try:
rel = plog.relative_to(logs_root)
out_name = str(rel).replace("/", "__")
except Exception:
out_name = plog.name
out_path = dest_dir / out_name
try:
shutil.copy2(plog, out_path)
copied += 1
except Exception:
pass

print(f"Collected {copied} Player.log file(s) into: {dest_dir}")
PY

# Try to collect crash reports (if any)
diagDir="$HOME/Library/Logs/DiagnosticReports"
if [ -d "$diagDir" ]; then
mkdir -p "$workspaceLogs/DiagnosticReports"
cp -R "$diagDir/"*Sample*Unity*6*macOS* "$workspaceLogs/DiagnosticReports/" 2>/dev/null || true
cp -R "$diagDir/"*Unity* "$workspaceLogs/DiagnosticReports/" 2>/dev/null || true
echo "Attempted to copy DiagnosticReports from: $diagDir"
else
echo "DiagnosticReports directory not found: $diagDir"
fi

rm -f "$zipPath" || true
(cd "${{ github.workspace }}/sample-unity6" && zip -r "Unity6-macOS-PlayerLogs.zip" "player-logs" >/dev/null) || true
echo "Player logs zip created: $zipPath"
- name: Upload Unity Player logs (macOS)
if: always()
uses: actions/upload-artifact@v4
with:
name: Unity6-macOS-Player-Logs
path: sample-unity6/Unity6-macOS-PlayerLogs.zip
- name: Remove temporary keychain
if: always()
run: |
Expand Down Expand Up @@ -316,6 +396,42 @@ jobs:
BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }}
working-directory: sample-unity6/Tests
run: python -m pytest -xs test/test_windows.py::WindowsTest
- name: Collect Unity Player logs (Windows)
if: always()
run: |
$playerLogRoot = Join-Path $env:USERPROFILE "AppData\LocalLow\Immutable\Sample Unity 6 Windows"
$workspaceLogs = "${{ github.workspace }}\sample-unity6\player-logs"
$zipPath = "${{ github.workspace }}\sample-unity6\Unity6-Windows-PlayerLogs.zip"

New-Item -ItemType Directory -Force -Path $workspaceLogs | Out-Null

if (Test-Path $playerLogRoot) {
Copy-Item -Force -Recurse "$playerLogRoot\*" $workspaceLogs -ErrorAction SilentlyContinue
Write-Output "Copied player logs from: $playerLogRoot"
} else {
Write-Output "Player log directory not found: $playerLogRoot"
}

# Also try to capture Unity crash dumps if present
$unityCrashes = Join-Path $env:USERPROFILE "AppData\LocalLow\Unity\Crashes"
if (Test-Path $unityCrashes) {
$crashDst = Join-Path $workspaceLogs "UnityCrashes"
New-Item -ItemType Directory -Force -Path $crashDst | Out-Null
Copy-Item -Force -Recurse "$unityCrashes\*" $crashDst -ErrorAction SilentlyContinue
Write-Output "Copied Unity crashes from: $unityCrashes"
} else {
Write-Output "Unity crashes directory not found: $unityCrashes"
}

if (Test-Path $zipPath) { Remove-Item -Force $zipPath }
Compress-Archive -Path "$workspaceLogs\*" -DestinationPath $zipPath -Force
Write-Output "Player logs zip created: $zipPath"
- name: Upload Unity Player logs (Windows)
if: always()
uses: actions/upload-artifact@v4
with:
name: Unity6-Windows-Player-Logs
path: sample-unity6/Unity6-Windows-PlayerLogs.zip
- name: Upload build log
if: always()
uses: actions/upload-artifact@v4
Expand Down
77 changes: 66 additions & 11 deletions sample/Tests/test/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,33 @@ def stop_altdriver(self):
if self.__class__.altdriver:
self.__class__.altdriver.stop()

def wait_for_output(
self,
output_obj,
predicate,
*,
timeout_seconds: float = 15.0,
poll_seconds: float = 0.25,
) -> str:
"""
Poll the `Output` UI element until `predicate(text)` is True.

UI actions often update `Output` asynchronously (especially in CI),
so reading immediately after `.tap()` can be flaky.
"""
deadline = time.time() + float(timeout_seconds)
last_text = ""
while time.time() < deadline:
try:
last_text = output_obj.get_text()
if predicate(last_text):
return last_text
except Exception:
# App/UI might be mid-transition; retry.
pass
time.sleep(float(poll_seconds))
return last_text

@pytest.mark.skip(reason="Base test should not be executed directly")
def test_0_other_functions(self):
# Show set call timeout scene
Expand All @@ -59,31 +86,52 @@ def test_1_passport_functions(self):
output = self.altdriver.find_object(By.NAME, "Output")

# Get access token
prev = output.get_text()
self.altdriver.find_object(By.NAME, "GetAccessTokenBtn").tap()
text = output.get_text()
self.assertTrue(len(text) > 50)
text = self.wait_for_output(
output,
lambda t: len(t) > 50 and (t != prev or prev == ""),
timeout_seconds=20,
)
self.assertTrue(len(text) > 50, f"Access token output too short. Actual output: '{text}'")

# Get ID token
prev = output.get_text()
self.altdriver.find_object(By.NAME, "GetIdTokenBtn").tap()
text = output.get_text()
self.assertTrue(len(text) > 50)
text = self.wait_for_output(
output,
lambda t: len(t) > 50 and (t != prev or prev == ""),
timeout_seconds=20,
)
self.assertTrue(len(text) > 50, f"ID token output too short. Actual output: '{text}'")

# Get email
self.altdriver.find_object(By.NAME, "GetEmail").tap()
text = output.get_text()
text = self.wait_for_output(
output,
lambda t: t == TestConfig.EMAIL,
timeout_seconds=10,
)
print(f"GetEmail output: {text}")
self.assertEqual(TestConfig.EMAIL, text)

# Get Passport ID
self.altdriver.find_object(By.NAME, "GetPassportId").tap()
text = output.get_text()
text = self.wait_for_output(
output,
lambda t: t == TestConfig.PASSPORT_ID,
timeout_seconds=10,
)
print(f"GetPassportId output: {text}")
self.assertEqual(TestConfig.PASSPORT_ID, text)

# Get linked addresses
self.altdriver.find_object(By.NAME, "GetLinkedAddresses").tap()
time.sleep(1)
text = output.get_text()
text = self.wait_for_output(
output,
lambda t: t == "No linked addresses",
timeout_seconds=10,
)
print(f"GetLinkedAddresses output: {text}")
self.assertEqual("No linked addresses", text)

Expand Down Expand Up @@ -207,14 +255,21 @@ def test_3_zkevm_functions(self):

# Connect to zkEVM
self.altdriver.find_object(By.NAME, "ConnectEvmBtn").tap()
text = output.get_text()
text = self.wait_for_output(
output,
lambda t: t == "Connected to EVM",
timeout_seconds=30,
)
print(f"ConnectEvmBtn output: {text}")
self.assertEqual("Connected to EVM", text)

# Initiliase wallet and get address
self.altdriver.wait_for_object(By.NAME, "RequestAccountsBtn").tap()
time.sleep(5)
text = output.get_text()
text = self.wait_for_output(
output,
lambda t: t == TestConfig.WALLET_ADDRESS,
timeout_seconds=30,
)
print(f"RequestAccountsBtn output: {text}")
self.assertEqual(TestConfig.WALLET_ADDRESS, text)

Expand Down
124 changes: 1 addition & 123 deletions sample/Tests/test/test_android.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,127 +106,5 @@ def test_2_other_functions(self):
def test_3_passport_functions(self):
self.test_1_passport_functions()

def test_4_imx_functions(self):
self.test_2_imx_functions()

def test_5_zkevm_functions(self):
self.test_3_zkevm_functions()

def test_6_pkce_relogin(self):
driver = self.appium_driver

self.close_and_open_app()

# Restart AltTester
self.altdriver.stop()
self.altdriver = AltDriver()
time.sleep(5)

# # Select use PKCE auth
self.altdriver.find_object(By.NAME, "PKCE").tap()
# Wait for unauthenticated screen
self.altdriver.wait_for_current_scene_to_be("UnauthenticatedScene")

# Relogin
print("Re-logging in...")
self.altdriver.wait_for_object(By.NAME, "ReloginBtn").tap()

# Wait for authenticated screen
self.altdriver.wait_for_current_scene_to_be("AuthenticatedScene")
print("Re-logged in")

# Get access token
self.altdriver.find_object(By.NAME, "GetAccessTokenBtn").tap()
output = self.altdriver.find_object(By.NAME, "Output")
self.assertTrue(len(output.get_text()) > 50)

# Click Connect to IMX button
self.altdriver.find_object(By.NAME, "ConnectBtn").tap()
time.sleep(5)
self.assertEqual("Connected to IMX", output.get_text())

self.altdriver.stop()

def test_7_pkce_reconnect(self):
self.close_and_open_app()

# Restart AltTester
self.altdriver.stop()
self.altdriver = AltDriver()
time.sleep(5)

# Select use PKCE auth
self.altdriver.find_object(By.NAME, "PKCE").tap()
# Wait for unauthenticated screen
self.altdriver.wait_for_current_scene_to_be("UnauthenticatedScene")

# Reconnect
print("Reconnecting...")
self.altdriver.wait_for_object(By.NAME, "ReconnectBtn").tap()

# Wait for authenticated screen
self.altdriver.wait_for_current_scene_to_be("AuthenticatedScene")
print("Reconnected")

# Get access token
self.altdriver.find_object(By.NAME, "GetAccessTokenBtn").tap()
output = self.altdriver.find_object(By.NAME, "Output")
self.assertTrue(len(output.get_text()) > 50)

# Get address without having to click Connect to IMX button
self.altdriver.find_object(By.NAME, "GetAddressBtn").tap()
self.assertEqual(TestConfig.WALLET_ADDRESS, output.get_text())

# Logout
print("Logging out...")
self.altdriver.find_object(By.NAME, "LogoutBtn").tap()
time.sleep(5)

# Wait for authenticated screen
self.altdriver.wait_for_current_scene_to_be("UnauthenticatedScene")
time.sleep(5)
print("Logged out")

self.altdriver.stop()

def test_8_pkce_connect_imx(self):
self.close_and_open_app()

# Restart AltTester
self.altdriver.stop()
self.altdriver = AltDriver()
time.sleep(5)

# Select use PKCE auth
self.altdriver.find_object(By.NAME, "PKCE").tap()
# Wait for unauthenticated screen
self.altdriver.wait_for_current_scene_to_be("UnauthenticatedScene")

# Connect IMX
print("Logging in and connecting to IMX...")
self.altdriver.wait_for_object(By.NAME, "ConnectBtn").tap()

self.login()

# Wait for authenticated screen
self.altdriver.wait_for_current_scene_to_be("AuthenticatedScene")
print("Logged in and connected to IMX")

# Get access token
self.altdriver.find_object(By.NAME, "GetAccessTokenBtn").tap()
output = self.altdriver.find_object(By.NAME, "Output")
self.assertTrue(len(output.get_text()) > 50)

# Get address without having to click Connect to IMX button
self.altdriver.find_object(By.NAME, "GetAddressBtn").tap()
self.assertEqual(TestConfig.WALLET_ADDRESS, output.get_text())

# Logout
print("Logging out...")
self.altdriver.find_object(By.NAME, "LogoutBtn").tap()
time.sleep(5)

# Wait for authenticated screen
self.altdriver.wait_for_current_scene_to_be("UnauthenticatedScene")
time.sleep(5)
print("Logged out")
self.test_3_zkevm_functions()
Loading
Loading