11module GenieTest
22
33using Test
4- export App, wait_for, notify_test
4+ export App, wait_for, notify_test, @App
55
66using Reexport
77@reexport using Stipple
@@ -10,13 +10,20 @@ using Reexport
1010using Electron
1111using UUIDs
1212
13- struct App
14- __model__:: Union{ReactiveModel, Nothing}
15- __window__:: Union{Window, Nothing}
16- __priority__:: Base.RefValue{Symbol}
13+ abstract type DummyWindow end
14+ Base. run (w:: Union{DummyWindow, Window} , code:: JSONText ) = run (w, json (code))
15+
16+ Base. @kwdef mutable struct App
17+ __model__:: Union{ReactiveModel, Nothing} = nothing
18+ __window__:: Union{Window, Nothing} = nothing
19+ __priority__:: Symbol = :model
20+ __url__:: String = " "
21+ __electron_options__:: Dict{String, Any} = Dict {String, Any} ()
22+ __timeout__:: Float64 = 30.0
23+ __port__:: Union{Int, Nothing} = nothing
1724end
1825
19- App (model :: Union{ReactiveModel, Nothing} , window :: Union{Window, Nothing} , priority :: Symbol = :model ) = App (model, window, Ref ( :model ))
26+ const AppDict = Dict{Any, App}
2027
2128"""
2229 unproxy(msg::String)
@@ -28,10 +35,10 @@ function unproxy(msg::String)
2835end
2936
3037function Base. getproperty (app:: App , fieldname:: Symbol )
31- fieldname ∈ [ :__model__ , :__window__ , :__priority__ ] && return getfield (app, fieldname)
38+ fieldname ∈ fieldnames (App) && return getfield (app, fieldname)
3239
33- if app. __priority__[] == :window && app. __window__ != = nothing
34- run (app. __window__, unproxy (" GENIEMODEL['$fieldname ']" ))
40+ if app. __priority__ == :window && app. __window__ != = nothing
41+ run (app. __window__, unproxy (" window?. GENIEMODEL?. ['$fieldname ']" ))
3542 elseif app. __model__ != = nothing
3643 model = getfield (app, :__model__ )
3744 if ! hasproperty (model, fieldname)
@@ -47,18 +54,16 @@ function Base.getproperty(app::App, fieldname::Symbol)
4754 field isa Reactive ? field[] : field
4855 end
4956 elseif app. __window__ != = nothing
50- try
51- run (app. __window__, unproxy (" GENIEMODEL['$fieldname ']" ))
52- catch _
53- nothing
54- end
57+ run (app. __window__, unproxy (" window?.GENIEMODEL?.['$fieldname ']" ))
5558 else
5659 @warn (" App has neither model nor window" )
5760 end
5861end
5962
6063function Base. setproperty! (app:: App , fieldname:: Symbol , value)
61- if app. __priority__[] == :window && app. __window__ != = nothing
64+ fieldname ∈ fieldnames (App) && return setfield! (app, fieldname, value)
65+
66+ if app. __priority__ == :window && app. __window__ != = nothing
6267 js_value = json (render (value))
6368 run (app. __window__, unproxy (" GENIEMODEL['$fieldname '] = $js_value " ))
6469 elseif app. __model__ != = nothing
@@ -96,10 +101,10 @@ Base.notify(app::App, msg::AbstractString, type::Union{Nothing, String, Symbol}
96101
97102"""
98103 App(url::String = "/";
99- timeout::Int = 30,
104+ timeout::Float64 = 30,
100105 port = nothing,
101106 id::String = string(uuid4()),
102- frontend::Symbol = :browser,
107+ frontend::Symbol = startswith(url, r"https://"i) || !backend ? :electron : :browser,
103108 backend::Bool = !startswith(url, r"https://"i),
104109 isready::Function = app -> app.isready
105110)
@@ -109,7 +114,7 @@ Create a Stipple App with optional frontend and backend.
109114- `url::String = "/"`: URL to open in the frontend. If it does not start with
110115 "http://" or "https://", it is assumed to be "http://localhost:port/".
111116# Keyword Arguments
112- - `timeout::Int = 10 `: Timeout in seconds to wait for the backend to be ready.
117+ - `timeout::Float64 = 30 `: Timeout in seconds to wait for the backend to be ready.
113118- `port = nothing`: Port where the Genie server is running. If `nothing`,
114119 it uses `Genie.config.server_port`.
115120- `id::String = string(uuid4())`: Debug ID used to identify the Stipple model
@@ -119,21 +124,22 @@ Create a Stipple App with optional frontend and backend.
119124 opens an Electron window. If `:none`, it does not open any frontend.
120125- `backend::Bool = true`: Whether to start the backend Stipple model. If `false`,
121126 only the frontend is started. This can be useful for testing remote apps.
122- - `backend_ready::Function = model -> model .isready[] `: Function to check if the
127+ - `backend_ready::Function = app -> app .isready === true `: Function to check if the
123128 backend is ready. It takes the Stipple model as argument and should return
124129 `true` if the backend is ready. By default, it checks the `isready` field
125130 of the model.
126131# Returns
127132An `App` instance containing the backend model and the frontend window.
128133"""
129134function App (url:: String ;
130- timeout:: Int = 10 ,
135+ timeout:: Real = 30 ,
131136 port = nothing ,
132137 id:: String = string (uuid4 ()),
133- frontend:: Symbol = :browser ,
134138 backend:: Bool = ! startswith (url, r" https://" i ),
139+ frontend:: Symbol = startswith (url, r" https://" i ) || ! backend ? :electron : :browser ,
135140 isready:: Function = app -> app. isready === true ,
136- electron_options:: Dict{String, <:Any} = Dict {String, Any} ()
141+ electron_options:: Dict{String, <:Any} = Dict {String, Any} (),
142+ priority:: Symbol = :model
137143)
138144 port === nothing && (port = Genie. config. server_port)
139145 println ()
@@ -161,15 +167,17 @@ function App(url::String;
161167 frontend == :none && (model. isready[] = true )
162168 end
163169
164- app = App (model, win)
170+ app = App (model, win, priority, " $url " , electron_options, float (timeout), port )
165171 if model === nothing && win === nothing
166172 @warn (" App has neither frontend nor backend" )
167173 return app
168174 end
169175 print (" Waiting for App to be ready " )
170- while ! isready (app) && time () < t0 + timeout
171- sleep (1 )
172- print (' .' )
176+ dt = time () - t0
177+ while ! isready (app) && dt < timeout
178+ delay = dt < 1 ? 0.1 : 1
179+ sleep (delay)
180+ delay > 1 && print (' .' )
173181 end
174182 println ()
175183
@@ -185,11 +193,29 @@ end
185193
186194function App (:: Type{T} ; kwargs... ) where T <: ReactiveModel
187195 model = Stipple. ReactiveTools. init_model (T; kwargs... )
188- return App (model, nothing )
196+ model. isready[] = true
197+ return App (__model__ = model; kwargs... )
189198end
190199
191200App (context:: Module ) = App (@eval context Stipple. @type )
192201
202+ macro App ()
203+ :(App (@__MODULE__ ))
204+ end
205+
206+ function Base. propertynames (app:: App )
207+ if app. __model__ != = nothing
208+ tuple (propertynames (app. __model__)... , fieldnames (App)... )
209+ elseif app. __window__ != = nothing
210+ fnames = run (app. __window__, """
211+ (x => Object.keys(x).filter(k => typeof x[k] !== 'function' && k !== 'WebChannel'))(window.GENIEMODEL || {})
212+ """ )
213+ tuple (Symbol .(fnames)... , fieldnames (App)... )
214+ else
215+ fieldnames (App)
216+ end
217+ end
218+
193219function Base. run (app:: App , msg:: Union{String, JSONText} )
194220 msg isa JSONText && (msg = json (msg))
195221 if app. __window__ != = nothing
@@ -244,4 +270,31 @@ function notify_test(app::App, test::Test.Result, test_str::AbstractString = "Te
244270 notify_test (app. __model__, test, test_str)
245271end
246272
273+ function connect! (app:: App ; timeout = nothing , port = nothing , isready:: Function = app -> app. isready === true )
274+ if app. __window__ != = nothing && ! app. __window__. exists
275+ @info " App window appears to be closed. Recreating the window..."
276+ try
277+ a = App (
278+ app. __url__,
279+ frontend = :electron ,
280+ electron_options = app. __electron_options__,
281+ priority = app. __priority__,
282+ backend = app. __model__ != = nothing ,
283+ timeout = timeout === nothing ? app. __timeout__ : timeout,
284+ port = port === nothing ? app. __port__ : port,
285+ )
286+ app. __model__ = a. __model__
287+ app. __window__ = a. __window__
288+ app. __port__ = a. __port__
289+ app. __timeout__ = a. __timeout__
290+ true
291+ catch e
292+ @warn " Failed to recreate app window: $e "
293+ false
294+ end
295+ else
296+ true
297+ end
298+ end
299+
247300end # GenieTest
0 commit comments