Wait for the native form's request to finish, not for 500ms
requestFinished watches the browser's network events for the POST and closes once it has finished or failed, so the next navigation never races the 204.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 file changed,
+40
−4
check/liveform_browser_test.go+40 −4
diff --git a/check/liveform_browser_test.go b/check/liveform_browser_test.go| index c05707a..fff3d54 100644 |
| --- a/check/liveform_browser_test.go |
| +++ b/check/liveform_browser_test.go |
| @@ -6,9 +6,11 @@ import ( |
| "context" |
| "net/http" |
| "strings" |
| + "sync" |
| "testing" |
| "time" |
| + "github.com/chromedp/cdproto/network" |
| "github.com/chromedp/chromedp" |
| "amadan.net/rastrillo/fiesta" |
| @@ -104,8 +106,9 @@ func TestBusyLeavesALiveFormItsScriptClaimed(t *testing.T) { |
| // Native: busy.js arms it, late, and the browser sends it. |
| var native string |
| - mustRun(t, ctx, chromedp.Navigate(rig.Origin+order), chromedp.WaitVisible("#native", chromedp.ByQuery), |
| - chromedp.Click("#native", chromedp.ByQuery), chromedp.Sleep(150*time.Millisecond), |
| + mustRun(t, ctx, chromedp.Navigate(rig.Origin+order), chromedp.WaitVisible("#native", chromedp.ByQuery)) |
| + answered := requestFinished(ctx, http.MethodPost, rig.Origin+"/native") |
| + mustRun(t, ctx, chromedp.Click("#native", chromedp.ByQuery), chromedp.Sleep(150*time.Millisecond), |
| chromedp.Evaluate(liveFormState("native"), &native)) |
| if want := "busy:true spin:yes"; native != want { |
| t.Errorf("%s: a form the live script left to the browser reads %q after the submit, want %q: busy.js did not arm it", order, native, want) |
| @@ -114,7 +117,40 @@ func TestBusyLeavesALiveFormItsScriptClaimed(t *testing.T) { |
| t.Errorf("%s: the native form sent %q, want form=native", order, got) |
| } |
| // The server has the form before the browser has its 204, and |
| - // a navigation started meanwhile is aborted by the answer. |
| - mustRun(t, ctx, chromedp.Sleep(500*time.Millisecond)) |
| + // a navigation started before the answer is aborted by it. |
| + select { |
| + case <-answered: |
| + case <-time.After(20 * time.Second): |
| + t.Fatalf("%s: the browser never finished the native form's request", order) |
| + } |
| } |
| } |
| + |
| +// requestFinished is closed once the browser has finished, or given |
| +// up on, the first request with this method and URL sent after the |
| +// call. |
| +func requestFinished(ctx context.Context, method, url string) <-chan struct{} { |
| + done := make(chan struct{}) |
| + var once sync.Once |
| + var id network.RequestID |
| + var mu sync.Mutex |
| + chromedp.ListenTarget(ctx, func(event any) { |
| + mu.Lock() |
| + defer mu.Unlock() |
| + switch e := event.(type) { |
| + case *network.EventRequestWillBeSent: |
| + if id == "" && e.Request.Method == method && e.Request.URL == url { |
| + id = e.RequestID |
| + } |
| + case *network.EventLoadingFinished: |
| + if id != "" && e.RequestID == id { |
| + once.Do(func() { close(done) }) |
| + } |
| + case *network.EventLoadingFailed: |
| + if id != "" && e.RequestID == id { |
| + once.Do(func() { close(done) }) |
| + } |
| + } |
| + }) |
| + return done |
| +} |