11defmodule PhoenixTest.Playwright.Connection do
22 @ moduledoc """
3- Stateful, `GenServer` based connection to a Playwright node.js server.
4- The connection is established via `PhoenixTest.Playwright.Port`.
3+ Stateful, `:gen_statem` based connection to a Playwright node.js server.
4+ The connection is established via `PhoenixTest.Playwright.PortServer`.
5+
6+ States:
7+ - `:pending`: Initial state, waiting for Playwright initialization. Post calls are postponed.
8+ - `:started`: Playwright is ready, all operations are processed normally.
59
610 You won't usually have to use this module directly.
711 `PhoenixTest.Playwright` uses this under the hood.
812 """
9- use GenServer
13+ @ behaviour :gen_statem
1014
1115 alias PhoenixTest.Playwright.Config
1216 alias PhoenixTest.Playwright.PortServer
1317
1418 @ timeout_grace_factor 1.5
1519 @ min_genserver_timeout to_timeout ( second: 1 )
1620
17- defstruct status: :pending ,
18- awaiting_started: [ ] ,
19- initializers: % { } ,
21+ defstruct initializers: % { } ,
2022 guid_subscribers: % { } ,
2123 posts_in_flight: % { }
2224
2325 @ name __MODULE__
2426
27+ @ doc false
2528 def start_link do
26- GenServer . start_link ( __MODULE__ , :no_init_arg , name: @ name , timeout: Config . global ( :timeout ) )
29+ :gen_statem . start_link ( { :local , @ name } , __MODULE__ , :no_init_arg , timeout: Config . global ( :timeout ) )
2730 end
2831
2932 @ doc """
3033 Launch a browser and return its `guid`.
3134 """
3235 def launch_browser ( type , opts ) do
3336 ensure_started ( )
34-
3537 types = initializer ( "Playwright" )
3638 type_id = Map . fetch! ( types , type ) . guid
37-
38- timeout =
39- opts [ :browser_launch_timeout ] || opts [ :timeout ] || Config . global ( :browser_launch_timeout )
40-
41- params =
42- opts
43- |> Map . new ( )
44- |> Map . put ( :timeout , timeout )
39+ timeout = opts [ :browser_launch_timeout ] || opts [ :timeout ] || Config . global ( :browser_launch_timeout )
40+ params = opts |> Map . new ( ) |> Map . put ( :timeout , timeout )
4541
4642 case post ( guid: type_id , method: :launch , params: params ) do
47- % { result: % { browser: % { guid: guid } } } ->
48- guid
49-
50- % { error: % { error: % { name: "TimeoutError" , stack: stack , message: message } } } ->
51- raise """
52- Timed out while launching the Playwright browser, #{ String . capitalize ( "#{ type } " ) } . #{ message }
53-
54- You may need to increase the :browser_launch_timeout option in config/test.exs:
55-
56- config :phoenix_test,
57- playwright: [
58- browser_launch_timeout: 10_000,
59- # other Playwright options...
60- ],
61- # other phoenix_test options...
62-
63- Playwright backtrace:
64-
65- #{ stack }
66- """
43+ % { result: % { browser: % { guid: guid } } } -> guid
44+ % { error: % { error: % { name: "TimeoutError" } = error } } -> raise launch_timeout_error_msg ( type , error )
6745 end
6846 end
6947
@@ -72,152 +50,140 @@ defmodule PhoenixTest.Playwright.Connection do
7250 nil -> start_link ( )
7351 pid -> { :ok , pid }
7452 end
75-
76- GenServer . call ( @ name , :awaiting_started )
7753 end
7854
7955 @ doc """
8056 Subscribe to messages for a guid and its descendants.
8157 """
8258 def subscribe ( pid \\ self ( ) , guid ) do
83- GenServer . cast ( @ name , { :subscribe , { pid , guid } } )
59+ :gen_statem . cast ( @ name , { :subscribe , pid , guid } )
8460 end
8561
86- @ doc """
87- Handle a parsed message from the PortServer.
88- This is called by PortServer after parsing complete messages from the Port.
89- """
62+ @ doc false
9063 def handle_playwright_msg ( msg ) do
91- GenServer . cast ( @ name , { :playwright_msg , msg } )
64+ :gen_statem . cast ( @ name , { :msg , msg } )
9265 end
9366
9467 @ doc """
9568 Post a message and await the response.
96- We wait for an additional grace period after the timeout that we pass to playwright.
69+ Wait for an additional grace period after the playwright timeout .
9770 """
9871 def post ( msg , timeout \\ nil ) do
99- default = % { params: % { } , metadata: % { } }
100-
10172 msg =
10273 msg
103- |> Enum . into ( default )
74+ |> Enum . into ( % { params: % { } , metadata: % { } } )
10475 |> update_in ( ~w( params timeout) a , & ( & 1 || timeout || Config . global ( :timeout ) ) )
76+ |> Map . put_new_lazy ( :id , fn -> System . unique_integer ( [ :positive , :monotonic ] ) end )
10577
10678 call_timeout = max ( @ min_genserver_timeout , round ( msg . params . timeout * @ timeout_grace_factor ) )
107- GenServer . call ( @ name , { :post , msg } , call_timeout )
79+
80+ :gen_statem . call ( @ name , { :post , msg } , call_timeout )
10881 end
10982
11083 @ doc """
11184 Get the initializer data for a channel.
11285 """
11386 def initializer ( guid ) do
114- GenServer . call ( @ name , { :initializer , guid } )
87+ :gen_statem . call ( @ name , { :initializer , guid } )
11588 end
11689
117- @ impl GenServer
90+ @ impl :gen_statem
91+ def callback_mode , do: :state_functions
92+
93+ @ impl :gen_statem
11894 def init ( :no_init_arg ) do
119- { :ok , _ } = PortServer . start_link ( self ( ) )
95+ { :ok , _port_server } = PortServer . start_link ( self ( ) )
12096 msg = % { guid: "" , params: % { sdk_language: :javascript } , method: :initialize , metadata: % { } }
12197 PortServer . post ( msg )
12298
123- { :ok , % __MODULE__ { } }
99+ { :ok , :pending , % __MODULE__ { } }
124100 end
125101
126- @ impl GenServer
127- def handle_cast ( { :subscribe , { recipient , guid } } , state ) do
128- subscribers = Map . update ( state . guid_subscribers , guid , [ recipient ] , & [ recipient | & 1 ] )
129- { :noreply , % { state | guid_subscribers: subscribers } }
102+ @ doc false
103+ def pending ( :cast , { :msg , % { method: :__create__ , params: % { guid: "Playwright" } } = msg } , data ) do
104+ { :next_state , :started , add_initializer ( data , msg ) }
130105 end
131106
132- def handle_cast ( { :playwright_msg , msg } , state ) do
133- state = handle_recv ( msg , state )
134- { :noreply , state }
135- end
107+ def pending ( :cast , _msg , _data ) , do: { :keep_state_and_data , [ :postpone ] }
108+ def pending ( { :call , _from } , _msg , _data ) , do: { :keep_state_and_data , [ :postpone ] }
136109
137- @ impl GenServer
138- def handle_call ( { :post , msg } , from , state ) do
139- msg_id = fn -> System . unique_integer ( [ :positive , :monotonic ] ) end
140- msg = msg |> Map . new ( ) |> Map . put_new_lazy ( :id , msg_id )
110+ @ doc false
111+ def started ( { :call , from } , { :post , msg } , data ) do
141112 PortServer . post ( msg )
142-
143- { :noreply , Map . update! ( state , :posts_in_flight , & Map . put ( & 1 , msg . id , from ) ) }
144- end
145-
146- def handle_call ( { :initializer , guid } , _from , state ) do
147- { :reply , Map . get ( state . initializers , guid ) , state }
113+ { :keep_state , put_in ( data . posts_in_flight [ msg . id ] , from ) }
148114 end
149115
150- def handle_call ( :awaiting_started , from , % { status: :pending } = state ) do
151- { :noreply , Map . update! ( state , :awaiting_started , & [ from | & 1 ] ) }
116+ def started ( { :call , from } , { :initializer , guid } , data ) do
117+ { :keep_state_and_data , [ { :reply , from , Map . fetch! ( data . initializers , guid ) } ] }
152118 end
153119
154- def handle_call ( :awaiting_started , _from , % { status: :started } = state ) do
155- { :reply , :ok , state }
120+ def started ( :cast , { :subscribe , recipient , guid } , data ) do
121+ { :keep_state , update_in ( data . guid_subscribers [ guid ] , & [ recipient | & 1 || [ ] ] ) }
156122 end
157123
158- defp handle_recv ( msg , state ) do
159- state
160- |> log_js ( msg )
161- |> add_initializer ( msg )
162- |> handle_started ( msg )
163- |> reply_in_flight ( msg )
164- |> send_to_subscribers ( msg )
165- end
166-
167- defp log_js ( state , % { method: :page_error } = msg ) do
124+ def started ( :cast , { :msg , % { method: :page_error } = msg } , _data ) do
168125 if module = Config . global ( :js_logger ) do
169126 module . log ( :error , msg . params . error , msg )
170127 end
171128
172- state
129+ :keep_state_and_data
173130 end
174131
175- defp log_js ( state , % { method: :console } = msg ) do
132+ def started ( :cast , { :msg , % { method: :console } = msg } , _data ) do
176133 if module = Config . global ( :js_logger ) do
177- level =
178- case msg [ :params ] [ :type ] do
179- "error" -> :error
180- "debug" -> :debug
181- _ -> :info
182- end
183-
134+ level = log_level_from_js ( msg [ :params ] [ :type ] )
184135 module . log ( level , msg . params . text , msg )
185136 end
186137
187- state
138+ :keep_state_and_data
188139 end
189140
190- defp log_js ( state , _ ) , do: state
141+ def started ( :cast , { :msg , msg } , data ) when is_map_key ( data . posts_in_flight , msg . id ) do
142+ { from , posts_in_flight } = Map . pop ( data . posts_in_flight , msg . id )
143+ :gen_statem . reply ( from , msg )
191144
192- defp handle_started ( state , % { method: :__create__ , params: % { type: "Playwright" } } ) do
193- for from <- state . awaiting_started , do: GenServer . reply ( from , :ok )
194- % { state | status: :started , awaiting_started: :none }
145+ { :keep_state , % { data | posts_in_flight: posts_in_flight } }
195146 end
196147
197- defp handle_started ( state , _ ) , do: state
198-
199- defp add_initializer ( state , % { method: :__create__ } = msg ) do
200- Map . update! ( state , :initializers , & Map . put ( & 1 , msg . params . guid , msg . params . initializer ) )
148+ def started ( :cast , { :msg , msg } , data ) do
149+ { :keep_state , data |> add_initializer ( msg ) |> notify_subscribers ( msg ) }
201150 end
202151
203- defp add_initializer ( state , _ ) , do: state
152+ defp add_initializer ( data , % { method: :__create__ } = msg ) do
153+ put_in ( data . initializers [ msg . params . guid ] , msg . params . initializer )
154+ end
204155
205- defp reply_in_flight ( % { posts_in_flight: in_flight } = state , msg ) when is_map_key ( in_flight , msg . id ) do
206- { from , in_flight } = Map . pop ( in_flight , msg . id )
207- GenServer . reply ( from , msg )
156+ defp add_initializer ( data , _msg ) , do: data
208157
209- % { state | posts_in_flight: in_flight }
158+ defp notify_subscribers ( data , msg ) when is_map_key ( data . guid_subscribers , msg . guid ) do
159+ for pid <- Map . fetch! ( data . guid_subscribers , msg . guid ) , do: send ( pid , { :playwright_msg , msg } )
160+ data
210161 end
211162
212- defp reply_in_flight ( state , _ ) , do: state
163+ defp notify_subscribers ( data , _msg ) , do: data
213164
214- defp send_to_subscribers ( state , % { guid: guid } = msg ) do
215- for pid <- Map . get ( state . guid_subscribers , guid , [ ] ) do
216- send ( pid , { :playwright , msg } )
217- end
165+ defp launch_timeout_error_msg ( type , error ) do
166+ % { stack: stack , message: message } = error
167+
168+ """
169+ Timed out while launching the Playwright browser, #{ String . capitalize ( "#{ type } " ) } . #{ message }
170+
171+ You may need to increase the :browser_launch_timeout option in config/test.exs:
172+
173+ config :phoenix_test,
174+ playwright: [
175+ browser_launch_timeout: 10_000,
176+ # other Playwright options...
177+ ],
178+ # other phoenix_test options...
179+
180+ Playwright backtrace:
218181
219- state
182+ #{ stack }
183+ """
220184 end
221185
222- defp send_to_subscribers ( state , _ ) , do: state
186+ defp log_level_from_js ( "error" ) , do: :error
187+ defp log_level_from_js ( "debug" ) , do: :debug
188+ defp log_level_from_js ( _ ) , do: :info
223189end
0 commit comments