Skip to content
This repository was archived by the owner on Jan 30, 2025. It is now read-only.

Commit bf9d7ff

Browse files
authored
Merge pull request #628 from grafana/refactor/593-waitforloadstate
Fix WaitForLoadState and add tests
2 parents 03a6747 + 8573887 commit bf9d7ff

File tree

5 files changed

+257
-26
lines changed

5 files changed

+257
-26
lines changed

common/frame.go

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1818,6 +1818,7 @@ func (f *Frame) WaitForFunction(fn goja.Value, opts goja.Value, jsArgs ...goja.V
18181818
}
18191819

18201820
// WaitForLoadState waits for the given load state to be reached.
1821+
// This will unblock if that lifecycle event has already been received.
18211822
func (f *Frame) WaitForLoadState(state string, opts goja.Value) {
18221823
f.log.Debugf("Frame:WaitForLoadState", "fid:%s furl:%q state:%s", f.ID(), f.URL(), state)
18231824
defer f.log.Debugf("Frame:WaitForLoadState:return", "fid:%s furl:%q state:%s", f.ID(), f.URL(), state)
@@ -1828,21 +1829,33 @@ func (f *Frame) WaitForLoadState(state string, opts goja.Value) {
18281829
k6ext.Panic(f.ctx, "parsing waitForLoadState %q options: %v", state, err)
18291830
}
18301831

1832+
timeoutCtx, timeoutCancel := context.WithTimeout(f.ctx, parsedOpts.Timeout)
1833+
defer timeoutCancel()
1834+
18311835
waitUntil := LifecycleEventLoad
18321836
if state != "" {
18331837
if err = waitUntil.UnmarshalText([]byte(state)); err != nil {
18341838
k6ext.Panic(f.ctx, "waiting for load state: %v", err)
18351839
}
18361840
}
18371841

1842+
lifecycleEvtCh, lifecycleEvtCancel := createWaitForEventPredicateHandler(
1843+
timeoutCtx, f, []string{EventFrameAddLifecycle},
1844+
func(data any) bool {
1845+
if le, ok := data.(LifecycleEvent); ok {
1846+
return le == waitUntil
1847+
}
1848+
return false
1849+
})
1850+
defer lifecycleEvtCancel()
1851+
18381852
if f.hasLifecycleEventFired(waitUntil) {
18391853
return
18401854
}
18411855

1842-
_, err = waitForEvent(f.ctx, f, []string{EventFrameAddLifecycle}, func(data any) bool {
1843-
return data.(LifecycleEvent) == waitUntil
1844-
}, parsedOpts.Timeout)
1845-
if err != nil {
1856+
select {
1857+
case <-lifecycleEvtCh:
1858+
case <-timeoutCtx.Done():
18461859
k6ext.Panic(f.ctx, "waiting for load state %q: %v", state, err)
18471860
}
18481861
}

common/helpers.go

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -198,26 +198,6 @@ func createWaitForEventPredicateHandler(
198198
return ch, evCancelFn
199199
}
200200

201-
func waitForEvent(
202-
ctx context.Context,
203-
emitter EventEmitter, events []string,
204-
predicateFn func(data any) bool,
205-
timeout time.Duration,
206-
) (any, error) { //nolint:unparam
207-
ch, evCancelFn := createWaitForEventHandler(ctx, emitter, events, predicateFn)
208-
defer evCancelFn() // Remove event handler
209-
210-
select {
211-
case <-ctx.Done():
212-
case <-time.After(timeout):
213-
return nil, fmt.Errorf("%w after %s", ErrTimedOut, timeout)
214-
case evData := <-ch:
215-
return evData, nil
216-
}
217-
218-
return nil, nil
219-
}
220-
221201
// panicOrSlowMo panics if err is not nil, otherwise applies slow motion.
222202
func panicOrSlowMo(ctx context.Context, err error) {
223203
if err != nil {

tests/lifecycle_wait_test.go

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,215 @@ import (
1414
"github.com/grafana/xk6-browser/common"
1515
)
1616

17+
func TestLifecycleWaitForLoadStateLoad(t *testing.T) {
18+
// Test description
19+
//
20+
// 1. goto /home and wait for the load lifecycle event.
21+
// 2. use WaitForLoadState with load to ensure that load
22+
// lifecycle event has already fired.
23+
//
24+
// Success criteria: We don't wait for all network requests to
25+
// complete, but we are interested in waiting
26+
// for all async scripts to have fully loaded
27+
// (which is when load is fired). We also want
28+
// to ensure that the load event is stored
29+
// internally, and we don't block on WaitForLoadState.
30+
31+
t.Parallel()
32+
33+
tb := newTestBrowser(t, withFileServer())
34+
p := tb.NewPage(nil)
35+
tb.withHandler("/home", func(w http.ResponseWriter, r *http.Request) {
36+
http.Redirect(w, r, tb.staticURL("wait_for_nav_lifecycle.html"), http.StatusMovedPermanently)
37+
})
38+
39+
var counter int64
40+
var counterMu sync.Mutex
41+
tb.withHandler("/ping", func(w http.ResponseWriter, _ *http.Request) {
42+
counterMu.Lock()
43+
defer counterMu.Unlock()
44+
45+
time.Sleep(time.Millisecond * 100)
46+
47+
counter++
48+
fmt.Fprintf(w, "pong %d", counter)
49+
})
50+
51+
tb.withHandler("/ping.js", func(w http.ResponseWriter, _ *http.Request) {
52+
fmt.Fprintf(w, `
53+
var pingJSTextOutput = document.getElementById("pingJSText");
54+
pingJSTextOutput.innerText = "ping.js loaded from server";
55+
`)
56+
})
57+
58+
waitUntil := common.LifecycleEventLoad
59+
assertHome(t, tb, p, waitUntil, func() {
60+
result := p.TextContent("#pingRequestText", nil)
61+
assert.NotEqualValues(t, "Waiting... pong 10 - for loop complete", result)
62+
63+
result = p.TextContent("#pingJSText", nil)
64+
assert.EqualValues(t, "ping.js loaded from server", result)
65+
66+
// This shouldn't block and return after calling hasLifecycleEventFired.
67+
p.WaitForLoadState(waitUntil.String(), nil)
68+
})
69+
}
70+
71+
func TestLifecycleWaitForLoadStateDOMContentLoaded(t *testing.T) {
72+
// Test description
73+
//
74+
// 1. goto /home and wait for the domcontentloaded lifecycle event.
75+
// 2. use WaitForLoadState with domcontentloaded to ensure that
76+
// domcontentloaded lifecycle event has already fired.
77+
//
78+
// Success criteria: We don't wait for all network requests or the
79+
// async scripts to complete, and we're only
80+
// interested in the html file being loaded. We
81+
// also want to ensure that the domcontentloaded
82+
// event is stored internally, and we don't block
83+
// on WaitForLoadState.
84+
85+
t.Parallel()
86+
87+
tb := newTestBrowser(t, withFileServer())
88+
p := tb.NewPage(nil)
89+
tb.withHandler("/home", func(w http.ResponseWriter, r *http.Request) {
90+
http.Redirect(w, r, tb.staticURL("wait_for_nav_lifecycle.html"), http.StatusMovedPermanently)
91+
})
92+
93+
var counter int64
94+
var counterMu sync.Mutex
95+
tb.withHandler("/ping", func(w http.ResponseWriter, _ *http.Request) {
96+
counterMu.Lock()
97+
defer counterMu.Unlock()
98+
99+
time.Sleep(time.Millisecond * 100)
100+
101+
counter++
102+
fmt.Fprintf(w, "pong %d", counter)
103+
})
104+
105+
tb.withHandler("/ping.js", func(w http.ResponseWriter, _ *http.Request) {
106+
fmt.Fprintf(w, `
107+
await new Promise(resolve => setTimeout(resolve, 1000));
108+
109+
var pingJSTextOutput = document.getElementById("pingJSText");
110+
pingJSTextOutput.innerText = "ping.js loaded from server";
111+
`)
112+
})
113+
114+
waitUntil := common.LifecycleEventDOMContentLoad
115+
assertHome(t, tb, p, waitUntil, func() {
116+
result := p.TextContent("#pingRequestText", nil)
117+
assert.NotEqualValues(t, "Waiting... pong 10 - for loop complete", result)
118+
119+
result = p.TextContent("#pingJSText", nil)
120+
assert.EqualValues(t, "Waiting...", result)
121+
122+
// This shouldn't block and return after calling hasLifecycleEventFired.
123+
p.WaitForLoadState(waitUntil.String(), nil)
124+
})
125+
}
126+
127+
func TestLifecycleWaitForLoadStateNetworkIdle(t *testing.T) {
128+
// Test description
129+
//
130+
// 1. goto /home and wait for the networkidle lifecycle event.
131+
// 2. use WaitForLoadState with networkidle to ensure that
132+
// networkidle lifecycle event has already fired.
133+
//
134+
// Success criteria: We wait for all network requests and async
135+
// scripts to complete. We also want to ensure
136+
// that the networkidle event is stored internally,
137+
// and we don't block on WaitForLoadState.
138+
139+
t.Parallel()
140+
141+
tb := newTestBrowser(t, withFileServer())
142+
p := tb.NewPage(nil)
143+
tb.withHandler("/home", func(w http.ResponseWriter, r *http.Request) {
144+
http.Redirect(w, r, tb.staticURL("wait_for_nav_lifecycle.html"), http.StatusMovedPermanently)
145+
})
146+
147+
var counter int64
148+
var counterMu sync.Mutex
149+
tb.withHandler("/ping", func(w http.ResponseWriter, _ *http.Request) {
150+
counterMu.Lock()
151+
defer counterMu.Unlock()
152+
153+
counter++
154+
fmt.Fprintf(w, "pong %d", counter)
155+
})
156+
157+
tb.withHandler("/ping.js", func(w http.ResponseWriter, _ *http.Request) {
158+
fmt.Fprintf(w, `
159+
var pingJSTextOutput = document.getElementById("pingJSText");
160+
pingJSTextOutput.innerText = "ping.js loaded from server";
161+
`)
162+
})
163+
164+
waitUntil := common.LifecycleEventNetworkIdle
165+
assertHome(t, tb, p, waitUntil, func() {
166+
result := p.TextContent("#pingRequestText", nil)
167+
assert.EqualValues(t, "Waiting... pong 10 - for loop complete", result)
168+
169+
result = p.TextContent("#pingJSText", nil)
170+
assert.EqualValues(t, "ping.js loaded from server", result)
171+
172+
// This shouldn't block and return after calling hasLifecycleEventFired.
173+
p.WaitForLoadState(waitUntil.String(), nil)
174+
})
175+
}
176+
177+
func TestLifecycleWaitForLoadStateDOMContentLoadedThenNetworkIdle(t *testing.T) {
178+
// Test description
179+
//
180+
// 1. goto /home and wait for the domcontentloaded lifecycle event.
181+
// 2. use WaitForLoadState with networkidle to now wait for the
182+
// lifecycle event from the browser.
183+
//
184+
// Success criteria: We want to quickly move to calling WaitForLoadState
185+
// so that we block until a networkidle lifecycle
186+
// event is received from the browser.
187+
188+
t.Parallel()
189+
190+
tb := newTestBrowser(t, withFileServer())
191+
p := tb.NewPage(nil)
192+
tb.withHandler("/home", func(w http.ResponseWriter, r *http.Request) {
193+
http.Redirect(w, r, tb.staticURL("wait_for_nav_lifecycle.html"), http.StatusMovedPermanently)
194+
})
195+
196+
var counter int64
197+
var counterMu sync.Mutex
198+
tb.withHandler("/ping", func(w http.ResponseWriter, _ *http.Request) {
199+
counterMu.Lock()
200+
defer counterMu.Unlock()
201+
202+
time.Sleep(time.Millisecond * 100)
203+
204+
counter++
205+
fmt.Fprintf(w, "pong %d", counter)
206+
})
207+
208+
tb.withHandler("/ping.js", func(w http.ResponseWriter, _ *http.Request) {
209+
fmt.Fprintf(w, `
210+
var pingJSTextOutput = document.getElementById("pingJSText");
211+
pingJSTextOutput.innerText = "ping.js loaded from server";
212+
`)
213+
})
214+
215+
assertHome(t, tb, p, common.LifecycleEventDOMContentLoad, func() {
216+
p.WaitForLoadState(common.LifecycleEventNetworkIdle.String(), nil)
217+
218+
result := p.TextContent("#pingRequestText", nil)
219+
assert.EqualValues(t, "Waiting... pong 10 - for loop complete", result)
220+
221+
result = p.TextContent("#pingJSText", nil)
222+
assert.EqualValues(t, "ping.js loaded from server", result)
223+
})
224+
}
225+
17226
func TestLifecycleReloadLoad(t *testing.T) {
18227
t.Parallel()
19228

tests/page_test.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -662,8 +662,6 @@ func TestPageWaitForFunction(t *testing.T) {
662662
func TestPageWaitForLoadState(t *testing.T) {
663663
t.Parallel()
664664

665-
// TODO: Add happy path tests once WaitForLoadState is not racy.
666-
667665
t.Run("err_wrong_event", func(t *testing.T) {
668666
t.Parallel()
669667

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<html>
2+
3+
<head></head>
4+
5+
<body>
6+
<a href="/home" id="homeLink">Home</a>
7+
<div id="pingRequestText">Waiting...</div>
8+
<div id="pingJSText">Waiting...</div>
9+
10+
<script>
11+
var pingRequestTextOutput = document.getElementById("pingRequestText");
12+
13+
var p = requestPings();
14+
p.then(() => {
15+
pingRequestTextOutput.innerText += ' - for loop complete';
16+
})
17+
18+
async function requestPings() {
19+
for (var i = 0; i < 10; i++) {
20+
await fetch('/ping')
21+
.then(response => response.text())
22+
.then((data) => {
23+
pingRequestTextOutput.innerText = 'Waiting... ' + data;
24+
});
25+
}
26+
}
27+
</script>
28+
<script src="/ping.js" async></script>
29+
</body>
30+
31+
</html>

0 commit comments

Comments
 (0)