Skip to content

Commit c49336a

Browse files
committed
feat!: Implement sobek script engine
This removes the experimental Goja JavaScript engine and replaces it with Sobek, a fork of Goja, but with ESM support
2 parents c138fb2 + 4d9bd64 commit c49336a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+1302
-866
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
# Temporary files while running tests
22
*.test
33
node_modules/
4+
*.log

dom/event/event_target.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,5 +227,5 @@ func (e *eventTarget) logger() (res *slog.Logger) {
227227
if res == nil {
228228
res = log.Default()
229229
}
230-
return res
230+
return
231231
}

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ require (
77
github.com/gost-dom/css v0.1.0
88
github.com/gost-dom/fixture v0.1.0
99
github.com/gost-dom/v8go v0.0.0-20250712111039-fd213ddc42d7
10+
github.com/grafana/sobek v0.0.0-20251113105955-976a34df9c09
1011
github.com/onsi/gomega v1.38.2
1112
github.com/stretchr/testify v1.11.1
1213
golang.org/x/net v0.47.0

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ github.com/gost-dom/fixture v0.1.0 h1:7PnEpkuww1QBOh4gyoR8updPfyosHJfxmFRY+2t05+
2424
github.com/gost-dom/fixture v0.1.0/go.mod h1:7mSGaBhOfi/NO+YiD426CQ1DTkko62vYxoSVRmveqqY=
2525
github.com/gost-dom/v8go v0.0.0-20250712111039-fd213ddc42d7 h1:l8UeevV8eXNZ3t40WrxoT6cfeGyYShV+I4mjfuOcSPc=
2626
github.com/gost-dom/v8go v0.0.0-20250712111039-fd213ddc42d7/go.mod h1:IiR7SWCB6ZLT7HgLd0TCjEaSHdGJvJqhhfujJMrv0Qk=
27+
github.com/grafana/sobek v0.0.0-20251103154147-6b40183f38e5 h1:Xq3nZfEjUR6E1TndDx+/8jkvX1x6ENDBvCV0zEW71wo=
28+
github.com/grafana/sobek v0.0.0-20251103154147-6b40183f38e5/go.mod h1:YtuqiJX1W3XvRSilL/kUZzduJG3phPJWyzM9DiIEfBo=
29+
github.com/grafana/sobek v0.0.0-20251113105955-976a34df9c09 h1:7hKNwELLbDrtpAggI21fqlVnbXS98zJ0pJIEs2t61wM=
30+
github.com/grafana/sobek v0.0.0-20251113105955-976a34df9c09/go.mod h1:YtuqiJX1W3XvRSilL/kUZzduJG3phPJWyzM9DiIEfBo=
2731
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
2832
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
2933
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=

internal/clock/clock.go

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -124,12 +124,21 @@ func New(options ...NewClockOption) *Clock {
124124
return c
125125
}
126126

127+
func (c *Clock) setLogger(l *slog.Logger) *slog.Logger {
128+
if l == nil {
129+
l = log.Default()
130+
}
131+
l = l.With("pkg", "gost-dom/clock")
132+
c.Logger = l
133+
return l
134+
}
135+
127136
func (c *Clock) logger() *slog.Logger {
128137
l := c.Logger
129138
if l == nil {
130-
l = log.Default()
139+
l = c.setLogger(nil)
131140
}
132-
return l.With("pkg", "gost-dom/clock")
141+
return l
133142
}
134143

135144
// runMicrotasksAndFlush runs first microtasks, e.g., tasks added using
@@ -198,6 +207,7 @@ func (c *Clock) runWhile(predicate func() bool) []error {
198207
// Returns an error if any of the added tasks generate an error. Panics if the
199208
// task list doesn't decrease in size. See [Clock] documentation for more info.
200209
func (c *Clock) Advance(d time.Duration) error {
210+
c.logger().Debug("Clock.Advance", "duration", d, "clock", c)
201211
endTime := c.Time.Add(d)
202212
errs := c.runMicrotasksAndFlush()
203213
errs = append(errs, c.runWhile(func() bool {
@@ -207,6 +217,13 @@ func (c *Clock) Advance(d time.Duration) error {
207217
return errors.Join(errs...)
208218
}
209219

220+
func (c *Clock) LogValue() slog.Value {
221+
return slog.GroupValue(
222+
slog.Int("noMicrotasks", len(c.microtasks)),
223+
slog.Int("noTasks", len(c.tasks)),
224+
)
225+
}
226+
210227
// Tick runs all tasks scheduled for immediate execution. This is synonymous
211228
// with calling Advance(0).
212229
func (c *Clock) Tick() error { return c.Advance(0) }
@@ -421,7 +438,9 @@ func (c *Clock) RunAll() error {
421438
// NewClockOption are used to initialize a new [Clock]
422439
type NewClockOption func(*Clock)
423440

424-
func WithLogger(l *slog.Logger) NewClockOption { return func(c *Clock) { c.Logger = l } }
441+
func WithLogger(l *slog.Logger) NewClockOption {
442+
return func(c *Clock) { c.setLogger(l) }
443+
}
425444

426445
// Initializes the clock's simulated time from a concrete [time.Time] value.
427446
func OfTime(t time.Time) NewClockOption {

internal/test/integration/test-app/content/public/datastar/datastar.rc6.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1879,4 +1879,3 @@ export {
18791879
I as stopPeeking,
18801880
he as watcher,
18811881
};
1882-
//# sourceMappingURL=datastar.js.map

internal/test/scripttests/datastar_suite.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,10 +101,12 @@ func RunDataStarTests(t *testing.T, e html.ScriptEngine) {
101101
win.HTMLDocument().GetHTMLElementById("echo-input-field").Focus()
102102
ctrl := controller.KeyboardController{Window: win}
103103
ctrl.SendKey(key.RuneToKey('a'))
104+
ctrl.SendKey(key.RuneToKey('b'))
105+
ctrl.SendKey(key.RuneToKey('c'))
104106
win.Clock().RunAll()
105107

106108
output := win.HTMLDocument().GetHTMLElementById("echo-output")
107-
assert.Equal(t, "a", output.TextContent())
109+
assert.Equal(t, "abc", output.TextContent())
108110

109111
})
110112
}

internal/test/scripttests/document_suite.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,3 +71,14 @@ func (s *DocumentTestSuite) TestNewDocument() {
7171
s.MustEval("Object.getPrototypeOf(actual).constructor.name"),
7272
"Actual constructor")
7373
}
74+
75+
func (s *DocumentTestSuite) TestQuerySelectorAll() {
76+
s.MustLoadHTML(
77+
`<body><div>0</div><div data-key="1">1</div><div data-key="2">2</div></body>`,
78+
)
79+
s.Expect(
80+
s.MustEval(
81+
"Array.from(document.querySelectorAll('[data-key]')).map(x => x.outerHTML).join(',')",
82+
),
83+
).To(Equal(`<div data-key="1">1</div>,<div data-key="2">2</div>`))
84+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package scripttests
2+
3+
import (
4+
"testing"
5+
6+
"github.com/gost-dom/browser/html"
7+
"github.com/gost-dom/browser/internal/testing/gosttest"
8+
. "github.com/gost-dom/browser/testing/gomega-matchers"
9+
"github.com/onsi/gomega"
10+
"github.com/stretchr/testify/assert"
11+
)
12+
13+
func RunDownloadScriptSuite(t *testing.T, e html.ScriptEngine) {
14+
t.Run("Load script from source", func(t *testing.T) {
15+
const indexHTML = `
16+
<!DOCTYPE html>
17+
<html>
18+
<head><script src="module.js"></script></head>
19+
<body>
20+
<h1>Module test</h1>
21+
<div id="tgt"></div>
22+
</body>
23+
</html>`
24+
const moduleJS = `
25+
document.addEventListener("DOMContentLoaded", () => {
26+
document.getElementById("tgt").textContent="CONTENT";
27+
})
28+
`
29+
server := gosttest.HttpHandlerMap{
30+
"/index.html": gosttest.StaticHTML(indexHTML),
31+
"/module.js": gosttest.StaticJS(moduleJS),
32+
}
33+
win, err := initBrowser(t, e, server).Open("https://example.com/index.html")
34+
assert.NoError(t, err)
35+
g := gomega.NewWithT(t)
36+
g.Expect(win.Document().GetElementById("tgt")).To(HaveTextContent("CONTENT"))
37+
38+
})
39+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package scripttests
2+
3+
import (
4+
"testing"
5+
6+
"github.com/gost-dom/browser/html"
7+
app "github.com/gost-dom/browser/internal/test/integration/test-app"
8+
"github.com/gost-dom/browser/internal/testing/browsertest"
9+
. "github.com/gost-dom/browser/internal/testing/gomega-matchers"
10+
"github.com/gost-dom/browser/internal/testing/htmltest"
11+
. "github.com/gost-dom/browser/testing/gomega-matchers"
12+
"github.com/onsi/gomega"
13+
)
14+
15+
func RunHtmxTests(t *testing.T, e html.ScriptEngine) {
16+
t.Run("Click to increment counter", func(t *testing.T) {
17+
t.Parallel()
18+
19+
expect := gomega.NewWithT(t).Expect
20+
server := app.CreateServer()
21+
b := htmltest.NewBrowserHelper(t, browsertest.InitBrowser(t, server, e))
22+
win, err := b.Open("/counter/index.html")
23+
expect(err).ToNot(HaveOccurred())
24+
counter := win.Document().GetElementById("counter").(html.HTMLElement)
25+
expect(counter).To(HaveInnerHTML(Equal("Count: 1")))
26+
counter.Click()
27+
counter = win.Document().GetElementById("counter").(html.HTMLElement)
28+
expect(counter).To(HaveInnerHTML(Equal("Count: 2")))
29+
})
30+
31+
t.Run("Click hx-get link", func(t *testing.T) {
32+
t.Parallel()
33+
34+
expect := gomega.NewWithT(t).Expect
35+
server := app.CreateServer()
36+
b := htmltest.NewBrowserHelper(t, browsertest.InitBrowser(t, server, e))
37+
win := b.OpenWindow("/navigation/page-a.html")
38+
expect(win.ScriptContext().Eval("window.pageA")).To(BeTrue())
39+
expect(win.ScriptContext().Eval("window.pageB")).To(BeNil())
40+
41+
// Click an hx-get link
42+
win.HTMLDocument().GetHTMLElementById("link-to-b").Click()
43+
44+
expect(
45+
win.ScriptContext().Eval("window.pageA"),
46+
).To(BeTrue(), "Script context cleared from first page")
47+
expect(win.ScriptContext().Eval("window.pageB")).To(
48+
BeTrue(), "Scripts executed on second page")
49+
expect(win.Document()).To(
50+
HaveH1("Page B"), "Page heading", "Heading updated, i.e. htmx swapped")
51+
expect(win.Location().Pathname()).To(Equal("/navigation/page-a.html"), "Location updated")
52+
})
53+
54+
t.Run("Clock boosted link", func(t *testing.T) {
55+
t.Parallel()
56+
57+
expect := gomega.NewWithT(t).Expect
58+
server := app.CreateServer()
59+
b := htmltest.NewBrowserHelper(t, browsertest.InitBrowser(t, server, e))
60+
win, err := b.Open("/navigation/page-a.html")
61+
expect(err).ToNot(HaveOccurred())
62+
63+
// Click an hx-boost link
64+
expect(win.ScriptContext().Eval("window.pageA")).ToNot(BeNil())
65+
expect(win.ScriptContext().Eval("window.pageA")).To(BeTrue())
66+
expect(win.ScriptContext().Eval("window.pageB")).To(BeNil())
67+
win.Document().GetElementById("link-to-b-boosted").(html.HTMLElement).Click()
68+
69+
expect(win.ScriptContext().Eval("window.pageA")).ToNot(BeNil(), "A")
70+
expect(win.ScriptContext().Eval("window.pageB")).ToNot(BeNil(), "B")
71+
expect(win.ScriptContext().Eval("window.pageA")).To(
72+
BeTrue(), "Script context cleared from first page")
73+
expect(win.ScriptContext().Eval("window.pageB")).To(
74+
BeTrue(), "Scripts executed on second page")
75+
expect(win.Document()).To(
76+
HaveH1("Page B"), "Page heading", "Heading updated, i.e. htmx swapped")
77+
expect(win.Location().Pathname()).To(Equal("/navigation/page-b.html"), "Location updated")
78+
})
79+
80+
t.Run("Form submit", func(t *testing.T) {
81+
t.Parallel()
82+
83+
expect := gomega.NewWithT(t).Expect
84+
server := app.CreateServer()
85+
b := htmltest.NewBrowserHelper(t, browsertest.InitBrowser(t, server, e))
86+
win, err := b.Open("/forms/form-1.html")
87+
expect(err).ToNot(HaveOccurred())
88+
i1 := win.Document().GetElementById("field-1")
89+
i1.SetAttribute("value", "Foo")
90+
91+
btn := win.Document().GetElementById("submit-btn").(html.HTMLElement)
92+
expect(len(server.Requests)).To(Equal(2), "No of requests _before_ click")
93+
btn.Click()
94+
expect(len(server.Requests)).To(Equal(3), "No of requests _after_ click")
95+
elm := win.Document().GetElementById("field-value-1")
96+
expect(elm).To(HaveTextContent("Foo"))
97+
})
98+
}

0 commit comments

Comments
 (0)