Skip to content

Commit 02725c4

Browse files
fix(windows): reduce login/deeplink flakiness in UI tests
- Capture the immutablerunner:// callback via browser performance logs and forward it to Unity by writing the deeplink into the registry. - Poll for deeplink registry updates in WindowsDeepLink so login can complete without relying on focus events. - Harden Windows test setup (browser profile/permissions/protocol association) and improve diagnostics (timeouts, Player.log tail on failure). - Make Passport token assertions resilient by waiting for async Output updates. TODO: Investigate intermittent 400/500 from IMX offchain registration.
1 parent 33a39cf commit 02725c4

4 files changed

Lines changed: 544 additions & 39 deletions

File tree

sample/Tests/test/test.py

Lines changed: 56 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,33 @@ def stop_altdriver(self):
3636
if self.__class__.altdriver:
3737
self.__class__.altdriver.stop()
3838

39+
def wait_for_output(
40+
self,
41+
output_obj,
42+
predicate,
43+
*,
44+
timeout_seconds: float = 15.0,
45+
poll_seconds: float = 0.25,
46+
) -> str:
47+
"""
48+
Poll the `Output` UI element until `predicate(text)` is True.
49+
50+
UI actions often update `Output` asynchronously (especially in CI),
51+
so reading immediately after `.tap()` can be flaky.
52+
"""
53+
deadline = time.time() + float(timeout_seconds)
54+
last_text = ""
55+
while time.time() < deadline:
56+
try:
57+
last_text = output_obj.get_text()
58+
if predicate(last_text):
59+
return last_text
60+
except Exception:
61+
# App/UI might be mid-transition; retry.
62+
pass
63+
time.sleep(float(poll_seconds))
64+
return last_text
65+
3966
@pytest.mark.skip(reason="Base test should not be executed directly")
4067
def test_0_other_functions(self):
4168
# Show set call timeout scene
@@ -59,31 +86,52 @@ def test_1_passport_functions(self):
5986
output = self.altdriver.find_object(By.NAME, "Output")
6087

6188
# Get access token
89+
prev = output.get_text()
6290
self.altdriver.find_object(By.NAME, "GetAccessTokenBtn").tap()
63-
text = output.get_text()
64-
self.assertTrue(len(text) > 50)
91+
text = self.wait_for_output(
92+
output,
93+
lambda t: len(t) > 50 and (t != prev or prev == ""),
94+
timeout_seconds=20,
95+
)
96+
self.assertTrue(len(text) > 50, f"Access token output too short. Actual output: '{text}'")
6597

6698
# Get ID token
99+
prev = output.get_text()
67100
self.altdriver.find_object(By.NAME, "GetIdTokenBtn").tap()
68-
text = output.get_text()
69-
self.assertTrue(len(text) > 50)
101+
text = self.wait_for_output(
102+
output,
103+
lambda t: len(t) > 50 and (t != prev or prev == ""),
104+
timeout_seconds=20,
105+
)
106+
self.assertTrue(len(text) > 50, f"ID token output too short. Actual output: '{text}'")
70107

71108
# Get email
72109
self.altdriver.find_object(By.NAME, "GetEmail").tap()
73-
text = output.get_text()
110+
text = self.wait_for_output(
111+
output,
112+
lambda t: t == TestConfig.EMAIL,
113+
timeout_seconds=10,
114+
)
74115
print(f"GetEmail output: {text}")
75116
self.assertEqual(TestConfig.EMAIL, text)
76117

77118
# Get Passport ID
78119
self.altdriver.find_object(By.NAME, "GetPassportId").tap()
79-
text = output.get_text()
120+
text = self.wait_for_output(
121+
output,
122+
lambda t: t == TestConfig.PASSPORT_ID,
123+
timeout_seconds=10,
124+
)
80125
print(f"GetPassportId output: {text}")
81126
self.assertEqual(TestConfig.PASSPORT_ID, text)
82127

83128
# Get linked addresses
84129
self.altdriver.find_object(By.NAME, "GetLinkedAddresses").tap()
85-
time.sleep(1)
86-
text = output.get_text()
130+
text = self.wait_for_output(
131+
output,
132+
lambda t: t == "No linked addresses",
133+
timeout_seconds=10,
134+
)
87135
print(f"GetLinkedAddresses output: {text}")
88136
self.assertEqual("No linked addresses", text)
89137

sample/Tests/test/test_windows.py

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from alttester import *
1313

1414
from test import TestConfig, UnityTest
15-
from test_windows_helpers import login, open_sample_app, launch_browser, bring_sample_app_to_foreground, stop_browser, stop_sample_app, logout_with_controlled_browser
15+
from test_windows_helpers import login, open_sample_app, launch_browser, bring_sample_app_to_foreground, stop_browser, stop_sample_app, logout_with_controlled_browser, get_product_name
1616

1717
class WindowsTest(UnityTest):
1818

@@ -120,11 +120,56 @@ def _perform_login(self):
120120
bring_sample_app_to_foreground()
121121

122122
# Wait for authenticated screen
123-
self.get_altdriver().wait_for_current_scene_to_be("AuthenticatedScene")
123+
# Default AltTester timeout for this command is ~20s; CI often needs longer,
124+
# especially when the browser auto-handles the deep-link without a dialog.
125+
self.get_altdriver().wait_for_current_scene_to_be("AuthenticatedScene", timeout=90)
124126
stop_browser()
125127
print("[SUCCESS] Login successful")
126128

127129
except Exception as err:
130+
# Dump Player.log tail to help diagnose why the deep-link callback
131+
# wasn't processed (or why Unity failed after receiving it).
132+
try:
133+
import os
134+
product_name = os.getenv("UNITY_APP_NAME", get_product_name())
135+
log_path = os.path.join(
136+
"C:\\Users\\WindowsBuildsdkServi\\AppData\\LocalLow\\Immutable",
137+
product_name,
138+
"Player.log",
139+
)
140+
print(f"Attempting to dump Unity Player.log tail: {log_path}")
141+
if os.path.exists(log_path):
142+
with open(log_path, "r", encoding="utf-8", errors="ignore") as f:
143+
lines = f.read().splitlines()
144+
# The tail is often dominated by AltTester noise. Print:
145+
# 1) last lines, and 2) last relevant lines (Passport/Immutable/URLs/errors).
146+
tail = lines[-200:] if len(lines) > 200 else lines
147+
print("----- Player.log (tail) -----")
148+
for line in tail:
149+
print(line)
150+
print("----- end Player.log (tail) -----")
151+
152+
needles = (
153+
"immutable",
154+
"passport",
155+
"launchauthurl",
156+
"passport_auth_url",
157+
"immutablerunner",
158+
"error",
159+
"exception",
160+
"gb:",
161+
)
162+
relevant = [ln for ln in lines if any(n in ln.lower() for n in needles)]
163+
relevant_tail = relevant[-200:] if len(relevant) > 200 else relevant
164+
print("----- Player.log (relevant tail) -----")
165+
for line in relevant_tail:
166+
print(line)
167+
print("----- end Player.log (relevant tail) -----")
168+
else:
169+
print("Player.log not found.")
170+
except Exception as e:
171+
print(f"Failed to dump Player.log: {e}")
172+
128173
stop_browser()
129174
raise SystemExit(f"Login failed: {err}")
130175

@@ -224,7 +269,7 @@ def test_6_relogin(self):
224269
self.get_altdriver().wait_for_object(By.NAME, "ReloginBtn").tap()
225270

226271
# Wait for authenticated screen
227-
self.get_altdriver().wait_for_current_scene_to_be("AuthenticatedScene")
272+
self.get_altdriver().wait_for_current_scene_to_be("AuthenticatedScene", timeout=90)
228273
print("Re-logged in")
229274

230275
# Get access token
@@ -251,7 +296,7 @@ def test_7_reconnect_connect_imx(self):
251296
self.get_altdriver().wait_for_object(By.NAME, "ReconnectBtn").tap()
252297

253298
# Wait for authenticated screen
254-
self.get_altdriver().wait_for_current_scene_to_be("AuthenticatedScene")
299+
self.get_altdriver().wait_for_current_scene_to_be("AuthenticatedScene", timeout=90)
255300
print("Reconnected")
256301

257302
# Get access token
@@ -304,7 +349,7 @@ def test_8_connect_imx(self):
304349
bring_sample_app_to_foreground()
305350

306351
# Wait for authenticated screen
307-
self.get_altdriver().wait_for_current_scene_to_be("AuthenticatedScene")
352+
self.get_altdriver().wait_for_current_scene_to_be("AuthenticatedScene", timeout=90)
308353
print("Logged in and connected to IMX")
309354
stop_browser()
310355

0 commit comments

Comments
 (0)