This document will try to explain the most essential concepts of Gost-DOM to get started, as well as limitations you should be aware of.
The recommended way is to export the root http.Handler, and connecting to this
from Gost-DOM. The root handler is the handler you would pass to http.Server
or http.ListenAndServe, but optionally configured without irrellevant
middlewares like logging/monitoring.
package server
import "net/http"
var RootHandler http.Handler
func init() {
mux := http.NewServeMux()
configureRoutes(mux)
RootHandler = mux
}The easiest way to start is to create browser.Browser passing the HTTP
handler. In this mode, you don't need to start the server. Gost will call your
http.Handler function directly, resulting in every call into your code being
in a stack trace originating from your tests.
package server_test
import (
"testing"
"myapp/server"
"github.com/gost-dom/browser"
"github.com/gost-dom/html"
"github.com/gost-dom/testing/gosttest"
)
func TestWebBrowser(t *testing.T) {
t.Parallel()
b := browser.New(
browser.WithScriptEngine(v8engine.DefaultEngine()),
// Alternatively (adjust imports accordingly), using Sobek, a pure Go
// JavaScript engine:
// browser.WithScriptEngine(sobekengine.DefaultEngine()),
browser.WithHandler(server.RootHttpHandler),
browser.WithLogger(gosttest.NewTestingLogger(t)),
browser.WithContext(t.Context()),
)
window, err := b.Open("http://example.com/") // Host is ignored
assert.NoError(t, err)
win.Document().GetElementById("test-button").(html.HTMLElement).Click()
resultField := win.Document().GetElementById("output-element")
assert.Equal(t, "The button was clicked", resultField.TextContent())
}Breakdown of the code:
browser.Newobviously creates a new browser instance. By default, this will be configured with a V8 script engine.browser.WithScriptEnginepasses a script engine to use.browser.WithHandleris the recommended way, connect the browser directly to the root HTTP handler, bypassing the TCP stack. This is not necessary. Without it, you need to start the server on a TCP port, as well as remember to close it afterwards.browser.WithLoggeraccepts an*slog.Loggerfrom the std slog package.gosttest.NewTestingLogger(t)returns a*Loggerthat writes log output to thetesting.TBinstance.browser.WithContext(t.Context())will cause the browser to automatically close when the test context is cancelled. This will free resources, and allow the V8 instance to be reused for a new test, reducing the overhead of configuring V8.
b.Open("http://example.com")opens the page, and returns anhtml.Window. The host name is ignored when having usedbrowser.WithHandler. In fact,Open("/")will work, but cookies don't, as they are associated with an origin.- The window implements the DOM API, adjusted for Go naming conventions (upper case letters. Attributes are accessed througn methods).
GetElementByIdreturns anElementfrom the DOM specification. ButFocus()is a method onHTMLElementfrom the HTML DOM specification, so this requires a type assertion.
Note
The gosttest.NewTestLogger() could be replaced by something like slogt.
But be sure to close the browser before the test completes, as closing the
browser can write log statements, and writing the test output after the test
completes will panic.
Warning
When using the browser.WithHandler option, NO outgoing HTTP requests
will be performed. This effectively means that you must serve all content
locally; JavaScript served from CDN will not work.
- Timeouts and the event loop for information about how delayed callbacks and the "event loop" works.
- Simulating user input to simulate user interactions.
There are some crucial points you should be aware of:
- JavaScript served from CDN doesn't work (in the recommended usage)
- The host name is ignored, but not quite! (in the recommended usage)
setTimeoutandsetIntervalhandlers require you to "advance the clock"
But cookies are associated with a host name, so the second version will not use cookies.
Be aware of which origin is considered "secure context". https and localhost
are considered secure, but non-local http are not.
Some Web APIs, e.g. Location services, require a secure context.
This doesn't affect Gost-DOM at the moment, but it's advised to use a secure origin, in order to not have to rewrite a lot of tests in the future.
Not all functions are implemented on the Window and the DOM objects, but they do adhere to the DOM API, and DOM HTML API, but adopted for Go:
- Functions start with upper-case letters to be exportable.
- IDL Attribute getters become functions with the same name. E.g.,
form.methodbecomesform.Method() - IDL Attribute setters becomes functions prefixed with
Set.form.method = "post"becomesform.SetMethod("post") - Functions thay may throw an error in JavaScript return an extra
errorvalue. E.g.,querySelectorthrows an error if the pattern is invalid. Go's version has two return valuesQuerySelector(pattern string) (Element, error).
So your existing knowledge about navigating and manipulating the DOM applies to Gost as well.
Each Browser instance has it's own isolated V8 instance, and each window
created from the browser has it's own context, i.e. it's own global scope,
allowing all tests to run in parallel.
There are two sets of attributes in the DOM, IDL Attributes and Content Attributes.
Content Attributes are what most would consider attributes. They exist in the HTML,
like the class content attribute in <div class="container"></div>. The data
attributes are accessible in JavaScript using e.g.,
getAttribute/setAttribute.
IDL attributes are properties on objects in the DOM, i.e., the JavaScript
objects, and they normally reflect a corresponding content attribute. E.g., an
anchor element has an href attribute.
<a id="my-link" href="https://example.com">Example</a>const a = document.getElementById("my-link")
console.log(a.getAttribute("href") // Logs https://example.com
console.log(a.href) // Logs https://example.comNormally, setting an IDL attribute updates the corresponding content attribute. But other behaviour can be attaced.
E.g., the link has IDL attributes for the different URL components, and assigning a new value to them will update the href content attribute.
a.patnhame = "/idl-attributes"
console.log(a.getAttribute("href")) // Logs https://example.com/idl-attributesSome IDL attributes have different names, e.g., the IDL attribute that
corresponds to the class content attribute is not called class but className.
By default, Gost does not write to stdout. You can inject a global logger
*log/slog.Logger calling SetDefaultLogger in the browser/logger package.
Each browser also supports you to inject a browser scoped logger.
browser := browser.New(
browser.WithLogger(logger), // *slog.Logger instance
browser.WithHandler(rootHTTPHandler),
)
win, _ := browser.Open(url)There isn't a concrete logging strategy, except all error cases should be logged.
- Most JavaScript API calls will mostly log a debug statement.
- Some internal Go calls will log at the debug level.
- Some high level functions log at info level, e.g.,
Window.Navigate. consolefunctions will log with the appropriate level.- Errors, including unhandled JavaScript errors will generate error logs
Gost-DOM is written with testing in mind, so piping log output to the
testing.T log can be helpful. Furthermore, error logs typically represent
scenarios where your code is behaving unexpectedly, and you'd want the test to
fail - even if assertions are passing.
Gost-DOM log errors in these cases:
- A JavaScript error is unhandled.
- A network error occurs.
- JavaScript code calls functions that are not yet supported.
Whether you want error level logs to automatically fail the test or not, the last part provides crucial information. The test is failing, not because of a bug in your code, but you use a feature not yet implemented in Gost-DOM (it's not you, it's us). The error message will include the URL where you can submit an issue.
A simple log handler could look like this.
type TestingLogHandler struct { testing.TB; allowErrors bool }
func (l TestingLogHandler) Enabled(_ context.Context, lvl slog.Level) bool { return lvl >= slog.LevelInfo }
func (l TestingLogHandler) Handle(_ context.Context, r slog.Record) error {
h.TB.Context().Err() != nil {
// Check if the context is cancelled to detect if the test has
// completed to avoid calling t.Log/t.Error (which panics).
// This can happen when constructing a browser using t.Context(). The
// browser will dispose resources _after_ the context is closed, and log
// statements written during cleanup would result in a panic
return nil
}
l.TB.Helper()
if r.Level < slog.LevelError || l.allowErrors {
l.TB.Logf("%v: %s", r.Level, r.Message)
} else {
l.TB.Errorf("%v: %s", r.Level, r.Message)
}
return nil
}
func (l TestingLogHandler) WithAttrs(attrs []slog.Attr) slog.Handler { return l }
func (l TestingLogHandler) WithGroup(name string) slog.Handler { return l }3rd party modules also exist to provide similar behaviour.