generated from matrix-org/.github
-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Run JS SDK in separate tabs rather than browsers
Fixes #107 and should be faster to run.
- Loading branch information
Showing
4 changed files
with
238 additions
and
132 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,108 @@ | ||
package chrome | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"os" | ||
"path/filepath" | ||
"strconv" | ||
"sync" | ||
"time" | ||
|
||
"github.com/chromedp/cdproto/runtime" | ||
"github.com/chromedp/chromedp" | ||
) | ||
|
||
// We only spin up a single chrome browser for all tests. | ||
// Each test hosts the JS SDK HTML/JS on a random high-numbered port and opens it in a new tab | ||
// for test isolation. | ||
var ( | ||
browserInstance *Browser | ||
browserInstanceMu = &sync.Mutex{} | ||
) | ||
|
||
// GlobalBrowser returns the browser singleton, making it if needed. | ||
func GlobalBrowser() (*Browser, error) { | ||
browserInstanceMu.Lock() | ||
defer browserInstanceMu.Unlock() | ||
if browserInstance == nil { | ||
var err error | ||
browserInstance, err = NewBrowser() | ||
return browserInstance, err | ||
} | ||
return browserInstance, nil | ||
} | ||
|
||
type Browser struct { | ||
Ctx context.Context // topmost chromedp context | ||
ctxCancel func() | ||
execAllocCancel func() | ||
} | ||
|
||
// Create and run a new Chrome browser. | ||
func NewBrowser() (*Browser, error) { | ||
ansiRedForeground := "\x1b[31m" | ||
ansiResetForeground := "\x1b[39m" | ||
|
||
colorifyError := func(format string, args ...any) { | ||
format = ansiRedForeground + time.Now().Format(time.RFC3339) + " " + format + ansiResetForeground | ||
fmt.Printf(format, args...) | ||
} | ||
opts := chromedp.DefaultExecAllocatorOptions[:] | ||
os.Mkdir("chromedp", os.ModePerm) // ignore errors to allow repeated runs | ||
wd, _ := os.Getwd() | ||
userDir := filepath.Join(wd, "chromedp") | ||
opts = append(opts, | ||
chromedp.UserDataDir(userDir), | ||
) | ||
// increase the WS timeout from 20s (default) to 30s as we see timeouts with 20s in CI | ||
opts = append(opts, chromedp.WSURLReadTimeout(30*time.Second)) | ||
|
||
allocCtx, allocCancel := chromedp.NewExecAllocator(context.Background(), opts...) | ||
ctx, cancel := chromedp.NewContext(allocCtx, chromedp.WithBrowserOption( | ||
chromedp.WithBrowserLogf(colorifyError), chromedp.WithBrowserErrorf(colorifyError), //chromedp.WithBrowserDebugf(log.Printf), | ||
)) | ||
|
||
browser := &Browser{ | ||
Ctx: ctx, | ||
ctxCancel: cancel, | ||
execAllocCancel: allocCancel, | ||
} | ||
return browser, chromedp.Run(ctx) | ||
} | ||
|
||
func (b *Browser) Close() { | ||
b.ctxCancel() | ||
b.execAllocCancel() | ||
} | ||
|
||
func (b *Browser) NewTab(baseJSURL string, onConsoleLog func(s string)) (*Tab, error) { | ||
tabCtx, closeTab := chromedp.NewContext(b.Ctx) | ||
err := chromedp.Run(tabCtx, | ||
chromedp.Navigate(baseJSURL), | ||
) | ||
if err != nil { | ||
return nil, fmt.Errorf("NewTab: failed to navigate to %s: %s", baseJSURL, err) | ||
} | ||
|
||
// Listen for console logs for debugging AND to communicate live updates | ||
chromedp.ListenTarget(tabCtx, func(ev interface{}) { | ||
switch ev := ev.(type) { | ||
case *runtime.EventConsoleAPICalled: | ||
for _, arg := range ev.Args { | ||
s, err := strconv.Unquote(string(arg.Value)) | ||
if err != nil { | ||
s = string(arg.Value) | ||
} | ||
onConsoleLog(s) | ||
} | ||
} | ||
}) | ||
|
||
return &Tab{ | ||
BaseURL: baseJSURL, | ||
Ctx: tabCtx, | ||
browser: b, | ||
cancel: closeTab, | ||
}, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
package chrome | ||
|
||
import ( | ||
"context" | ||
"embed" | ||
"fmt" | ||
"io/fs" | ||
"net" | ||
"net/http" | ||
"sync" | ||
) | ||
|
||
//go:embed dist | ||
var jsSDKDistDirectory embed.FS | ||
|
||
type JSSDKInstanceOpts struct { | ||
// The specific port this instance should be hosted on. | ||
// This is crucial for persistent storage which relies on a stable port number | ||
// across restarts. | ||
Port int | ||
} | ||
|
||
// NewJSSDKWebsite hosts the JS SDK HTML/JS on a random high-numbered port | ||
// and runs a Go web server to serve those files. | ||
func NewJSSDKWebsite(opts JSSDKInstanceOpts) (baseURL string, close func(), err error) { | ||
// strip /dist so /index.html loads correctly as does /assets/xxx.js | ||
c, err := fs.Sub(jsSDKDistDirectory, "dist") | ||
if err != nil { | ||
return "", nil, fmt.Errorf("failed to strip /dist off JS SDK files: %s", err) | ||
} | ||
// run js-sdk (need to run this as a web server to avoid CORS errors you'd otherwise get with file: URLs) | ||
var wg sync.WaitGroup | ||
wg.Add(1) | ||
mux := &http.ServeMux{} | ||
mux.Handle("/", http.FileServer(http.FS(c))) | ||
srv := &http.Server{ | ||
Addr: fmt.Sprintf("127.0.0.1:%d", opts.Port), | ||
Handler: mux, | ||
} | ||
startServer := func() { | ||
ln, err := net.Listen("tcp", srv.Addr) | ||
if err != nil { | ||
panic(err) | ||
} | ||
baseURL = "http://" + ln.Addr().String() | ||
fmt.Println("JS SDK listening on", baseURL) | ||
wg.Done() | ||
srv.Serve(ln) | ||
fmt.Println("JS SDK closing webserver") | ||
} | ||
go startServer() | ||
wg.Wait() | ||
return baseURL, func() { | ||
srv.Close() | ||
}, nil | ||
} | ||
|
||
// Tab represents an open JS SDK instance tab | ||
type Tab struct { | ||
BaseURL string | ||
Ctx context.Context // tab context | ||
browser *Browser // a ref to the browser which made this tab | ||
closeServer func() | ||
cancel func() // closes the tab | ||
} | ||
|
||
func (t *Tab) Close() { | ||
t.cancel() | ||
if t.closeServer != nil { | ||
t.closeServer() | ||
} | ||
} | ||
|
||
func (t *Tab) SetCloseServer(close func()) { | ||
t.closeServer = close | ||
} |
Oops, something went wrong.