1
+ import pytest
2
+ import asyncio
3
+ import uuid
4
+ from unittest .mock import MagicMock , AsyncMock , patch , call , PropertyMock
5
+
6
+ from lmnr .sdk .client .synchronous .sync_client import LaminarClient
7
+ from lmnr .sdk .client .asynchronous .async_client import AsyncLaminarClient
8
+ from lmnr .sdk .browser .playwright_otel import PlaywrightInstrumentor
9
+
10
+
11
+ class TestBrowserEvents :
12
+ @pytest .fixture
13
+ def mock_sync_client (self ):
14
+ client = MagicMock (spec = LaminarClient )
15
+ client ._browser_events = MagicMock ()
16
+ client ._browser_events .send = MagicMock ()
17
+ return client
18
+
19
+ @pytest .fixture
20
+ def mock_async_client (self ):
21
+ client = AsyncMock (spec = AsyncLaminarClient )
22
+ client ._browser_events = AsyncMock ()
23
+ client ._browser_events .send = AsyncMock ()
24
+ return client
25
+
26
+ @pytest .fixture
27
+ def mock_sync_page (self ):
28
+ page = MagicMock (name = "SyncPage" )
29
+ page .evaluate .return_value = True
30
+ page .goto .return_value = None
31
+ page .click .return_value = None
32
+ page .is_closed .side_effect = [False , False , True ] # Return False twice, then True
33
+ return page
34
+
35
+ @pytest .fixture
36
+ def mock_async_page (self ):
37
+ page = AsyncMock (name = "AsyncPage" )
38
+ page .evaluate .return_value = True
39
+ page .goto .return_value = None
40
+ page .click .return_value = None
41
+ page .is_closed .side_effect = [False , False , True ] # Return False twice, then True
42
+ return page
43
+
44
+ @patch ('time.sleep' ) # Mock sleep to speed up tests
45
+ def test_sync_browser_events (self , mock_sleep , mock_sync_client , mock_sync_page ):
46
+ """Test that browser events are captured and sent to Laminar in synchronous mode"""
47
+ # Set up the evaluate method to return events
48
+ def evaluate_side_effect (js_code ):
49
+ if "lmnrGetAndClearEvents" in js_code :
50
+ # Return mock events on the second call
51
+ return [{"data" : [1 , 2 , 3 , 4 ]}]
52
+ return True
53
+
54
+ mock_sync_page .evaluate .side_effect = evaluate_side_effect
55
+
56
+ # Directly test the send_events_sync function
57
+ from lmnr .sdk .browser .pw_utils import send_events_sync
58
+
59
+ # Create test session_id and trace_id
60
+ session_id = str (uuid .uuid4 ().hex )
61
+ trace_id = format (12345 , "032x" ) # Simple trace ID for testing
62
+
63
+ # Send events
64
+ send_events_sync (mock_sync_page , session_id , trace_id , mock_sync_client )
65
+
66
+ # Verify events were sent
67
+ assert mock_sync_client ._browser_events .send .called , "No events were sent to Laminar"
68
+
69
+ # Verify call arguments
70
+ args = mock_sync_client ._browser_events .send .call_args [0 ]
71
+ assert len (args ) == 3 , "send() should receive 3 arguments: session_id, trace_id, events"
72
+
73
+ # Verify data format
74
+ sent_session_id , sent_trace_id , events = args
75
+ assert sent_session_id == session_id , "session_id mismatch"
76
+ assert sent_trace_id == trace_id , "trace_id mismatch"
77
+ assert isinstance (events , list ), "events should be a list"
78
+ assert len (events ) > 0 , "events list should not be empty"
79
+
80
+ @patch ('asyncio.sleep' ) # Mock sleep to speed up tests
81
+ @pytest .mark .asyncio
82
+ async def test_async_browser_events (self , mock_sleep , mock_async_client , mock_async_page ):
83
+ """Test that browser events are captured and sent to Laminar in asynchronous mode"""
84
+ # Set up the evaluate method to return events
85
+ async def evaluate_side_effect (js_code ):
86
+ if "lmnrGetAndClearEvents" in js_code :
87
+ # Return mock events
88
+ return [{"data" : [1 , 2 , 3 , 4 ]}]
89
+ return True
90
+
91
+ mock_async_page .evaluate .side_effect = evaluate_side_effect
92
+
93
+ # Directly test the send_events_async function
94
+ from lmnr .sdk .browser .pw_utils import send_events_async
95
+
96
+ # Create test session_id and trace_id
97
+ session_id = str (uuid .uuid4 ().hex )
98
+ trace_id = format (67890 , "032x" ) # Simple trace ID for testing
99
+
100
+ # Send events
101
+ await send_events_async (mock_async_page , session_id , trace_id , mock_async_client )
102
+
103
+ # Verify events were sent
104
+ assert mock_async_client ._browser_events .send .called , "No events were sent to Laminar"
105
+
106
+ # Verify call arguments
107
+ args = mock_async_client ._browser_events .send .call_args .args
108
+ assert len (args ) == 3 , "send() should receive 3 arguments: session_id, trace_id, events"
109
+
110
+ # Verify data format
111
+ sent_session_id , sent_trace_id , events = args
112
+ assert sent_session_id == session_id , "session_id mismatch"
113
+ assert sent_trace_id == trace_id , "trace_id mismatch"
114
+ assert isinstance (events , list ), "events should be a list"
115
+ assert len (events ) > 0 , "events list should not be empty"
116
+
117
+ @patch ('time.sleep' ) # Mock sleep to speed up tests
118
+ def test_multiple_pages (self , mock_sleep , mock_sync_client ):
119
+ """Test that events are properly captured from multiple pages"""
120
+ # Create two mock pages
121
+ mock_page1 = MagicMock (name = "Page1" )
122
+ mock_page1 .evaluate .return_value = [{"data" : [1 , 2 , 3 , 4 ]}]
123
+ mock_page1 .goto .return_value = None
124
+ mock_page1 .is_closed .return_value = False
125
+
126
+ mock_page2 = MagicMock (name = "Page2" )
127
+ mock_page2 .evaluate .return_value = [{"data" : [5 , 6 , 7 , 8 ]}]
128
+ mock_page2 .goto .return_value = None
129
+ mock_page2 .is_closed .return_value = False
130
+
131
+ # Directly test the send_events_sync function
132
+ from lmnr .sdk .browser .pw_utils import send_events_sync
133
+
134
+ # Create test session_id and trace_id
135
+ session_id = str (uuid .uuid4 ().hex )
136
+ trace_id = "0" * 32 # Simplified trace ID for testing
137
+
138
+ # Send events from both pages
139
+ send_events_sync (mock_page1 , session_id , trace_id , mock_sync_client )
140
+ send_events_sync (mock_page2 , session_id , trace_id , mock_sync_client )
141
+
142
+ # Verify events were sent twice
143
+ assert mock_sync_client ._browser_events .send .call_count == 2 , "Events should be sent twice"
144
+
145
+ # Verify the session_id was consistent across calls
146
+ session_ids = set ()
147
+ for call_args in mock_sync_client ._browser_events .send .call_args_list :
148
+ session_ids .add (call_args [0 ][0 ]) # First positional arg is session_id
149
+
150
+ assert len (session_ids ) == 1 , "There should be exactly one session ID used"
151
+
152
+ def test_instrumentor_structure (self , mock_sync_client ):
153
+ """Test the structure of the instrumentor and its wrapped methods"""
154
+ # Import the wrapped methods data structures
155
+ from lmnr .sdk .browser .playwright_otel import WRAPPED_METHODS , WRAPPED_METHODS_ASYNC
156
+
157
+ # Verify that the wrapped methods have the expected structure
158
+ expected_methods = [
159
+ 'new_page' , 'launch' , 'new_context' , 'close' ,
160
+ 'connect' , 'connect_over_cdp' , 'launch_persistent_context'
161
+ ]
162
+
163
+ expected_objects = ['BrowserContext' , 'Browser' , 'BrowserType' ]
164
+
165
+ # Check sync methods
166
+ sync_methods = set ()
167
+ for method in WRAPPED_METHODS :
168
+ assert method ['package' ] == 'playwright.sync_api' , f"Unexpected package for { method ['method' ]} "
169
+ assert method ['object' ] in expected_objects , f"Unexpected object for { method ['method' ]} "
170
+ assert method ['method' ] in expected_methods , f"Unexpected method: { method ['method' ]} "
171
+ assert callable (method ['wrapper' ]), f"Wrapper for { method ['method' ]} is not callable"
172
+ sync_methods .add (method ['method' ])
173
+
174
+ # Check async methods
175
+ async_methods = set ()
176
+ for method in WRAPPED_METHODS_ASYNC :
177
+ assert method ['package' ] == 'playwright.async_api' , f"Unexpected package for { method ['method' ]} "
178
+ assert method ['object' ] in expected_objects , f"Unexpected object for { method ['method' ]} "
179
+ assert method ['method' ] in expected_methods , f"Unexpected method: { method ['method' ]} "
180
+ assert callable (method ['wrapper' ]), f"Wrapper for { method ['method' ]} is not callable"
181
+ async_methods .add (method ['method' ])
182
+
183
+ # Verify all expected methods are covered
184
+ for method in expected_methods :
185
+ assert method in sync_methods , f"Missing sync method: { method } "
186
+ assert method in async_methods , f"Missing async method: { method } "
187
+
188
+ # Verify the instrumentor is initialized correctly
189
+ instrumentor = PlaywrightInstrumentor (
190
+ client = mock_sync_client ,
191
+ async_client = AsyncMock ()
192
+ )
193
+
194
+ # Check client references
195
+ assert instrumentor .client == mock_sync_client , "Client reference mismatch"
196
+
197
+ # Verify instrumentation dependencies
198
+ assert instrumentor .instrumentation_dependencies () == ("playwright >= 1.9.0" ,), "Unexpected instrumentation dependencies"
0 commit comments