Skip to content
25 changes: 25 additions & 0 deletions internal/testing/htmltest/script_context_helper.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package htmltest

import (
"testing"

"github.com/gost-dom/browser/html"
)

type ScriptContextHelper struct {
html.ScriptContext
t testing.TB
}

func NewScriptContextHelper(t testing.TB, ctx html.ScriptContext) ScriptContextHelper {
return ScriptContextHelper{ctx, t}
}

func (h ScriptContextHelper) MustEval(script string) any {
h.t.Helper()
res, err := h.Eval(script)
if err != nil {
h.t.Fatalf("Script error: %v", err)
}
return res
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
2 changes: 0 additions & 2 deletions scripting/internal/js/arguments.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,6 @@ func ConsumeArgument[T, U any](
}
}

func IsUndefined[T any](v Value[T]) bool { return v == nil || v.IsUndefined() }

func ConsumeRestArguments[T, U any](
args CallbackContext[U],
name string,
Expand Down
6 changes: 6 additions & 0 deletions scripting/internal/js/array.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package js

type Array[T any] interface {
Value[T]
Push(Value[T]) error
}
3 changes: 2 additions & 1 deletion scripting/internal/js/callback_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ type ValueFactory[T any] interface {

NewPromise() Promise[T]
NewString(string) Value[T]
NewNumber(float64) Value[T]
NewBoolean(bool) Value[T]
NewObject() Object[T]
NewFunction(Callback[T]) Function[T]
Expand All @@ -179,7 +180,7 @@ type ValueFactory[T any] interface {

// NewArray creates a JavaScript array containing the values. If any value
// is nil, it will become undefined in the resulting array.
NewArray(...Value[T]) Value[T]
NewArray(...Value[T]) Array[T]
// NewIterator returns an object implementing the [Iterator protocol]
//
// [Iterator protocol]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#the_iterator_protocol
Expand Down
120 changes: 115 additions & 5 deletions scripting/internal/js/value.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
package js

import (
"errors"
"fmt"

"github.com/gost-dom/browser/internal/constants"
)

// Value represents a value in JavaScript. Referential equality cannot be used
// to check if to Value instances represent the same value in JavaScript. Use
// StrictEquals to check if two values are equal.
Expand All @@ -15,6 +22,7 @@ type Value[T any] interface {
Self() T

String() string
Number() float64
Int32() int32
Uint32() uint32
Boolean() bool
Expand All @@ -23,16 +31,24 @@ type Value[T any] interface {
IsNull() bool
IsSymbol() bool
IsString() bool
IsNumber() bool
IsObject() bool
IsBoolean() bool
IsFunction() bool
IsArray() bool

AsFunction() (Function[T], bool)
AsObject() (Object[T], bool)

StrictEquals(Value[T]) bool
}

// IsNullish returns whether a JavaScript value is null or undefined.
func IsNullish[T any](v Value[T]) bool { return v == nil || v.IsNull() || v.IsUndefined() }

func IsUndefined[T any](v Value[T]) bool { return v == nil || v.IsUndefined() }
func IsBoolean[T any](v Value[T]) bool { return v != nil && v.IsBoolean() }

type Function[T any] interface {
Value[T]

Expand Down Expand Up @@ -100,14 +116,108 @@ type Promise[T any] interface {
Reject(Value[T])
}

// IsNullish returns whether a JavaScript value is null or undefined.
func IsNullish[T any](v Value[T]) bool { return v == nil || v.IsNull() || v.IsUndefined() }

func IsBoolean[T any](v Value[T]) bool { return v != nil && v.IsBoolean() }

func AsFunction[T any](v Value[T]) (Function[T], bool) {
if IsNullish(v) {
return nil, false
}
return v.AsFunction()
}

func Clone[T any](v Value[T], s Scope[T]) (Value[T], error) {
var objects [][2]Value[T]
return clone(v, s, &objects)
}

Comment thread
coderabbitai[bot] marked this conversation as resolved.
func clone[T any](v Value[T], s Scope[T], objects *[][2]Value[T]) (Value[T], error) {
switch {
case v == nil || v.IsUndefined():
return s.Undefined(), nil
case v.IsNull():
return s.Null(), nil
case v.IsUndefined():
return s.Undefined(), nil
case v.IsString():
return s.NewString(v.String()), nil
case v.IsNumber():
return s.NewNumber(v.Number()), nil
case v.IsBoolean():
Comment thread
coderabbitai[bot] marked this conversation as resolved.
return s.NewBoolean(v.Boolean()), nil
case v.IsArray():
return cloneArray(v, s, objects)
case v.IsFunction():
//TODO: Use correct error
return nil, errors.New("Serialize function")
}
if o, ok := v.AsObject(); ok {
return cloneObject(o, s, objects)
}
return nil, fmt.Errorf("Unable to clone value: %v", v)
}
Comment on lines +131 to +155
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Remove the duplicate IsUndefined branch to avoid dead code.
The first case already handles undefined values, so the later branch is unreachable.

♻️ Proposed cleanup
case v == nil || v.IsUndefined():
	return s.Undefined(), nil
case v.IsNull():
	return s.Null(), nil
-case v.IsUndefined():
-	return s.Undefined(), nil
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func clone[T any](v Value[T], s Scope[T], objects *[][2]Value[T]) (Value[T], error) {
switch {
case v == nil || v.IsUndefined():
return s.Undefined(), nil
case v.IsNull():
return s.Null(), nil
case v.IsUndefined():
return s.Undefined(), nil
case v.IsString():
return s.NewString(v.String()), nil
case v.IsNumber():
return s.NewNumber(v.Number()), nil
case v.IsBoolean():
return s.NewBoolean(v.Boolean()), nil
case v.IsArray():
return cloneArray(v, s, objects)
case v.IsFunction():
//TODO: Use correct error
return nil, errors.New("Serialize function")
}
if o, ok := v.AsObject(); ok {
return cloneObject(o, s, objects)
}
return nil, fmt.Errorf("Unable to clone value: %v", v)
}
func clone[T any](v Value[T], s Scope[T], objects *[][2]Value[T]) (Value[T], error) {
switch {
case v == nil || v.IsUndefined():
return s.Undefined(), nil
case v.IsNull():
return s.Null(), nil
case v.IsString():
return s.NewString(v.String()), nil
case v.IsNumber():
return s.NewNumber(v.Number()), nil
case v.IsBoolean():
return s.NewBoolean(v.Boolean()), nil
case v.IsArray():
return cloneArray(v, s, objects)
case v.IsFunction():
//TODO: Use correct error
return nil, errors.New("Serialize function")
}
if o, ok := v.AsObject(); ok {
return cloneObject(o, s, objects)
}
return nil, fmt.Errorf("Unable to clone value: %v", v)
}


func findKnownValue[T any](o Value[T], knownObjects *[][2]Value[T]) (Value[T], bool) {
for _, pair := range *knownObjects {
known := pair[0]
res := pair[1]
if o.StrictEquals(known) {
return res, true
}
}
return nil, false
}

func cloneArray[T any](
v Value[T],
s Scope[T],
knownObjects *[][2]Value[T],
) (Value[T], error) {
if existing, ok := findKnownValue(v, knownObjects); ok {
return existing, nil
}
o, ok := v.AsObject()
if !ok {
return nil, fmt.Errorf(
"Object was an array, but not convertible to object. %w",
constants.ErrGostDomBug,
)
}
res := s.NewArray()
*knownObjects = append(*knownObjects, [2]Value[T]{v, res})

for v, err := range Iterate(o) {
if err != nil {
return nil, err
}
cloned, err := clone(v, s, knownObjects)
if err != nil {
return nil, err
}
res.Push(cloned)
}
// TODO: Potential bug here, if the array references itself recursively,
// this would lead to a stack overflow error.
return res, nil
Comment on lines +196 to +198
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

TODO comment appears outdated—cycles are already handled.

The comment suggests recursive arrays could cause stack overflow, but cycle detection via findKnownValue at lines 173-175 should prevent this. The array is registered in knownObjects before iterating its elements, so recursive references will return the existing clone.

Consider removing or updating this TODO to reflect the actual behavior.

}
Comment on lines +168 to +199
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for Iterate function definition
rg -n 'func\s+Iterate' -C 5 --type=go

# Search for Array interface definition
rg -n 'type\s+Array\[' -C 5 --type=go

# Search for Push method
rg -n 'func.*\bPush\b' -C 3 --type=go

# Search for Length method
rg -n 'func.*\bLength\b' -C 3 --type=go

Repository: gost-dom/browser

Length of output: 4509


🏁 Script executed:

#!/bin/bash
# Get the full Iterate function implementation
sed -n '117,140p' scripting/internal/js/object.go

# Also check what happens after line 140 in that function
sed -n '117,170p' scripting/internal/js/object.go

# Look at how Object iteration is handled (for properties/indices)
rg -n 'for.*range.*Object' -C 3 --type=go

# Check v8engine array iteration implementation
cat scripting/v8engine/array.go

# Check sobekengine array implementation
cat scripting/sobekengine/array.go

Repository: gost-dom/browser

Length of output: 3674


🏁 Script executed:

#!/bin/bash
# Search for sparse array handling or tests
rg -i 'sparse' --type=go

# Search for structured-clone references
rg -i 'structured.*clone|clone.*structured' --type=go

# Look at the full clone function to understand the context
sed -n '140,200p' scripting/internal/js/value.go

# Search for any tests related to cloning
fd -e test.go -o -e _test.go | xargs rg -l 'clone|Clone' | head -5

Repository: gost-dom/browser

Length of output: 2133


🏁 Script executed:

#!/bin/bash
# Search for array length access or index-based methods
rg -n '\.Length\(\)|\.Get.*\[|for.*i.*<.*Length' -C 2 --type=go | head -50

# Look for tests directory and check for array cloning tests
find . -type f -name '*test*.go' | xargs rg -l 'clone.*[Aa]rray|[Aa]rray.*clone' 2>/dev/null

# Check for any Get(index) or SetIndex methods on Array
rg -n 'func.*\(.*Array\).*Get|func.*\(.*Array\).*SetIndex' -C 2 --type=go

# Search for how array indices are accessed in the codebase
rg -n 'Get.*\(\s*[0-9]' -C 2 --type=go | head -30

Repository: gost-dom/browser

Length of output: 4134


🏁 Script executed:

#!/bin/bash
# Let's verify JavaScript array iterator behavior with sparse arrays
# Search for any documentation or comments about array iteration and sparse arrays
rg -i 'hole|sparse|empty.*slot|undefined.*index' -C 3 --type=go

# Check if there are any tests that demonstrate array cloning behavior
fd '.*test.*\.go$' | xargs grep -l 'cloneArray\|clone.*Array' 2>/dev/null

# Look at the complete clone function signature and how cloneObject handles this
sed -n '140,220p' scripting/internal/js/value.go

# Search for reference to structured clone spec
rg -n 'spec|standard|algorithm' -i --type=go | grep -i clone

Repository: gost-dom/browser

Length of output: 4926


🏁 Script executed:

#!/bin/bash
# Check if there's an Array.length getter/setter or any length preservation
rg -n 'func.*Length|\.length|array\.length' -i -C 2 --type=go | head -50

# Search for array iteration tests or examples
find . -type f -name '*.go' | xargs grep -l 'Iterate.*array\|array.*Iterate' 2>/dev/null

# Check how NewArray is created and if it has any special properties
rg -n 'NewArray\(\)' -C 5 --type=go

# Look for any references to array.length preservation
rg -n 'Length.*array|array.*Length' -i -C 3 --type=go | grep -i 'set\|prop\|clone' | head -20

# Check the v8engine and sobekengine implementations of array length
rg -n 'func.*\(.*array\).*Length|func.*Length.*\(.*array' -C 3 --type=go

Repository: gost-dom/browser

Length of output: 3547


🏁 Script executed:

#!/bin/bash
# Check if array length is set after cloning
sed -n '168,210p' scripting/internal/js/value.go

# Search for any special array handling after iteration
rg -n 'res\.(Push|Set.*length|Length)' -C 3 --type=go

# Look for how arrays are created and if length is preserved
rg -n 'NewArray' -C 8 --type=go | head -60

# Check if there's any reference to JavaScript array length property
rg -n '\.Get\s*\(\s*"length"' -C 2 --type=go

# Check what the Iterate function returns for sparse arrays specifically
# by looking at how Symbol.iterator is handled
rg -n 'Symbol.*iterator|Symbol\.iterator' -i -C 3 --type=go

Repository: gost-dom/browser

Length of output: 12689


🏁 Script executed:

#!/bin/bash
# Let's verify the actual behavior with a comprehensive search
# Check if array.length is accessed anywhere after cloning
rg -n 'Get.*"length"|Get.*length' -C 5 --type=go | grep -A 5 -B 5 clone

# Check how v8engine's NewArray works - does it handle sparse arrays?
sed -n '200,220p' scripting/v8engine/callback_context.go

# Check if there's any code that reads array.length property
rg -n '\.Get\s*\(\s*"length"\s*\)' -C 3 --type=go

# Look for any tests that verify cloning behavior
fd '.*test.*\.go$' -exec grep -l 'structuredClone\|clone.*array\|sparse' {} \;

# Check if Iterate behavior is documented anywhere
rg -n 'Iterate' -B 5 -A 10 scripting/internal/js/object.go | head -80

Repository: gost-dom/browser

Length of output: 2429


🏁 Script executed:

#!/bin/bash
# Search for any clone tests or sparse array tests
find . -type f -name '*test.go' | head -10 | xargs grep -l 'clone\|Clone' 2>/dev/null

# Look for structured clone in tests
find . -type f -name '*test.go' -exec grep -l 'structuredClone' {} \;

# Check if there are any array cloning tests
find . -type f -name '*test.go' -exec grep -l 'Array\|array' {} \; | xargs grep -l 'clone' 2>/dev/null

# Verify: does Iterate call the array's Symbol.iterator which should iterate all indices?
# Let's check ObjectGetIterator
rg -n 'func ObjectGetIterator' -A 15 --type=go

# Most importantly: after cloning with Push, is the array.length set correctly?
# Let's search for any length assignment or restoration
rg -n 'res\.(length|Length|Set.*length)' -C 3 --type=go

Repository: gost-dom/browser

Length of output: 1120


🏁 Script executed:

#!/bin/bash
# Let's check what the actual behavior of Iterate is for sparse arrays
# by looking at the ObjectEnumerableOwnPropertyKeys which seems related
rg -n 'ObjectEnumerableOwnPropertyKeys|ObjectOwnPropertyKeys' -A 20 --type=go

# Check PropertyDescriptor to understand enumerable property handling
rg -n 'type PropertyDescriptor' -A 10 --type=go

# See how keys/indices are enumerated in ObjectEnumerableOwnPropertyKeys
sed -n '200,250p' scripting/internal/js/object.go

# Check if array length is being preserved after cloning anywhere
rg -n 'array.*length|length.*array' -i -C 3 --type=go | grep -i 'clone\|set'

Repository: gost-dom/browser

Length of output: 4696


🏁 Script executed:

#!/bin/bash
# Critical check: does the cloneArray function preserve array length?
# Let's look at the full function and what comes after the Push loop
sed -n '168,210p' scripting/internal/js/value.go

# Check if there's any array.length reading or restoration after cloning
rg -n 'v\.Get\("length"\)|o\.Get\("length"' -C 5 --type=go | grep -A 10 -B 10 clone

# Verify what JavaScript's Array iterator actually does
# Let's search for any documentation or comments about this
rg -n 'Array.*iterator|iterator.*Array' -i -C 3 --type=go

# Most importantly - check if after cloning, the array length is set
sed -n '168,200p' scripting/internal/js/value.go

Repository: gost-dom/browser

Length of output: 1874


🏁 Script executed:

#!/bin/bash
# The key question: does JavaScript's Array iterator skip holes or yield them as undefined?
# Let's search for documentation or tests that clarify this
rg -n 'Symbol\.iterator|iterator.*hole|hole.*iterator' -i -C 3 --type=go

# Check if there's any use of array indices (0, 1, 2...) vs just using iterator protocol
rg -n 'for.*i.*:=.*0.*i.*<|Get.*[0-9]|Get.*i|SetIndex|GetIndex' -C 2 --type=go | head -60

# Look for cloning tests to see if sparse arrays are tested
find . -type f -name '*.go' -path '*/test*' -o -name '*_test.go' | xargs grep -l 'Clone\|clone' 2>/dev/null | head -5

# Check how other code handles array length preservation
rg -n '\.Get.*length|\.Set.*length' -C 2 --type=go | grep -i array

Repository: gost-dom/browser

Length of output: 8797


🏁 Script executed:

#!/bin/bash
# Critical verification: Check if array.length is being accessed or preserved
# during or after cloning
rg -n 'v\.Get\("length"\)|o\.Get\("length"\)|Get.*Length' -C 8 --type=go | grep -B 8 -A 8 'cloneArray\|clone'

# Look specifically at what happens after the cloneArray function
sed -n '168,210p' scripting/internal/js/value.go

# Check if the original array's length is ever read
rg -n 'Get.*"length"|Get.*Length' scripting/internal/js/value.go

# Let's verify the exact behavior by checking what Iterate does
# Get the complete Iterate implementation
sed -n '117,170p' scripting/internal/js/object.go

Repository: gost-dom/browser

Length of output: 2238


Preserve sparse array structure during cloning.

The cloneArray implementation converts sparse arrays to dense arrays. JavaScript's Array iterator yields undefined for holes (it doesn't skip them), and pushing those values into a new array makes holes into explicit undefined elements, violating structured-clone behavior. Additionally, the original array's length is never read or preserved, which could cause length mismatches if the source array has non-integer properties or custom descriptors.

Consider reading the original array's length property and preserving array structure through index-based cloning rather than relying on the iterator protocol, or ensure all hole handling aligns with the structured-clone algorithm.


func cloneObject[T any](o Object[T], s Scope[T], knownObjects *[][2]Value[T]) (Value[T], error) {
if existing, ok := findKnownValue(o, knownObjects); ok {
return existing, nil
}
res := s.NewObject()
*knownObjects = append(*knownObjects, [2]Value[T]{o, res})
keys, err := o.Keys()
if err != nil {
return nil, err
}
for _, k := range keys {
oldV, err := o.Get(k)
if err != nil {
return nil, err
}
newV, err := clone(oldV, s, knownObjects)
if err != nil {
return nil, err
}
res.Set(k, newV)
}
return res, nil
}
96 changes: 96 additions & 0 deletions scripting/internal/scripttests/engine_suites.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package scripttests

import (
"fmt"
"testing"

"github.com/gost-dom/browser/html"
"github.com/gost-dom/browser/internal/entity"
"github.com/gost-dom/browser/internal/testing/htmltest"
"github.com/gost-dom/browser/scripting/internal/js"
"github.com/stretchr/testify/assert"
)

// RunScriptEngineSuites runs test suites of the script engine without a
// predefined global scope. In contrast to [RunSuites] that expect an engine
// configured for the global scope in the Window realm.
func RunScriptEngineSuites[T any](t *testing.T, f ScriptEngineFactory[T]) {
type Global = entity.Entity

e := f(js.ConfigurerFunc[T](func(e js.ScriptEngine[T]) {
global := e.ConfigureGlobalScope("Global", nil)
global.CreateOperation("store", func(ctx js.CallbackContext[T]) (js.Value[T], error) {
v, ok := ctx.ConsumeArg()
if !ok {
return nil, ctx.NewTypeError("Missing argument")
}
c, err := js.As[entity.Components](ctx.GlobalThis().NativeValue(), nil)
if err != nil {
return nil, err
}
entity.SetComponentType(c, v)
return nil, nil
})
Comment on lines +22 to +33
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Guard against nil js.Value from ConsumeArg.

ConsumeArg can yield a nil js.Value[T] (e.g., undefined). Storing nil risks clone failures later; treat it as a missing argument.

🛠️ Proposed fix
-		global.CreateOperation("store", func(ctx js.CallbackContext[T]) (js.Value[T], error) {
-			v, ok := ctx.ConsumeArg()
-			if !ok {
-				return nil, ctx.NewTypeError("Missing argument")
-			}
+		global.CreateOperation("store", func(ctx js.CallbackContext[T]) (js.Value[T], error) {
+			v, ok := ctx.ConsumeArg()
+			if !ok || v == nil {
+				return nil, ctx.NewTypeError("Missing argument")
+			}
 			c, err := js.As[entity.Components](ctx.GlobalThis().NativeValue(), nil)
 			if err != nil {
 				return nil, err
 			}

Based on learnings: In the gost-dom JavaScript binding system, js.Value[T] is a Go interface that can be nil even though it represents a JavaScript value. Always check v != nil before calling methods like v.Boolean() to prevent panics when the Go interface is nil.


global.CreateOperation("get", func(ctx js.CallbackContext[T]) (js.Value[T], error) {
c, err := js.As[entity.Components](ctx.GlobalThis().NativeValue(), nil)
if err != nil {
return nil, err
}
val, ok := entity.ComponentType[js.Value[T]](c)
if !ok {
return nil, fmt.Errorf("Value missing")
}

res, err := js.Clone(val, ctx)
if err != nil {
t.Errorf("Clone error: %v", err)
}
return res, err
Comment on lines +45 to +49
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Return early on clone error to avoid returning partial results.

Line 47 logs the error with t.Errorf but execution continues, potentially returning a nil res with a non-nil err. Consider returning immediately after logging or restructuring the error handling.

♻️ Proposed fix
 			res, err := js.Clone(val, ctx)
 			if err != nil {
-				t.Errorf("Clone error: %v", err)
+				return nil, fmt.Errorf("Clone error: %w", err)
 			}
 			return res, err

Or if you want to log and return:

 			res, err := js.Clone(val, ctx)
 			if err != nil {
 				t.Errorf("Clone error: %v", err)
+				return nil, err
 			}
-			return res, err
+			return res, nil
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
res, err := js.Clone(val, ctx)
if err != nil {
t.Errorf("Clone error: %v", err)
}
return res, err
res, err := js.Clone(val, ctx)
if err != nil {
return nil, fmt.Errorf("Clone error: %w", err)
}
return res, err
Suggested change
res, err := js.Clone(val, ctx)
if err != nil {
t.Errorf("Clone error: %v", err)
}
return res, err
res, err := js.Clone(val, ctx)
if err != nil {
t.Errorf("Clone error: %v", err)
return nil, err
}
return res, nil

})
}))

global1 := new(Global)
global2 := new(Global)

c1 := htmltest.NewScriptContextHelper(
t,
e.NewHost(html.ScriptEngineOptions{}).NewContext(dummyContext{global1, t.Context()}),
)
c2 := htmltest.NewScriptContextHelper(
t,
e.NewHost(html.ScriptEngineOptions{}).NewContext(dummyContext{global2, t.Context()}),
)

assert.NoError(t, c1.Run(`
const b = {
id: "b",
}
const arr = [1,2,3]
const recursiveArray = [1,2,3]
recursiveArray.push(recursiveArray)
const a = {
stringVal: "hello",
numberVal: 42.5,
trueVal: true,
falseVal: false,
b1: b,
b2: b,
arr1: arr,
arr2: arr,
recursiveArray,
}
globalThis.store(a)
`))
val, ok := entity.ComponentType[js.Value[T]](global1)
assert.True(t, ok)
entity.SetComponentType(global2, val)

assert.NoError(t, c2.Run("const cloned = globalThis.get()"))
assert.Equal(t, "hello", c2.MustEval("cloned.stringVal"))
assert.Equal(t, 42.5, c2.MustEval("cloned.numberVal"))
assert.True(t, c2.MustEval("cloned.trueVal").(bool))
assert.False(t, c2.MustEval("cloned.falseVal").(bool))
assert.True(t, c2.MustEval("cloned.b1 === cloned.b2").(bool))
assert.Equal(t, "1,2,3", c2.MustEval("cloned.arr1.join(',')"))
}
19 changes: 18 additions & 1 deletion scripting/internal/scripttests/suites.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
package scripttests

import (
"context"
"log/slog"
"net/http"
"testing"

"github.com/gost-dom/browser/html"
"github.com/gost-dom/browser/internal/entity"
"github.com/gost-dom/browser/internal/testing/browsertest"
"github.com/gost-dom/browser/scripting/internal/dom/domsuite"
"github.com/gost-dom/browser/scripting/internal/html/htmlsuite"
"github.com/gost-dom/browser/scripting/internal/js"
"github.com/gost-dom/browser/scripting/internal/uievents/uieventssuite"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
Expand All @@ -28,9 +33,11 @@ func RunBasicSuite(t *testing.T, e html.ScriptEngine) {
assert.True(t, w.MustEval("window === globalThis").(bool))
}

type ScriptEngineFactory[T any] = func(js.Configurer[T]) html.ScriptEngine

func RunSuites(t *testing.T, e html.ScriptEngine) {
t.Run("ScriptEngineBehaviour", func(t *testing.T) { testScriptEngineBehaviour(t, e) })
t.Run("SharowRoot", runSuite(NewShadowRootSuite(e)))
t.Run("ShadowRoot", runSuite(NewShadowRootSuite(e)))
t.Run("DocumentFragment", runSuite(NewDocumentFragmentSuite(e)))
t.Run("XMLHttpRequest", runSuite(NewXMLHttpRequestSuite(e)))
t.Run("Location", runSuite(NewLocationSuite(e)))
Expand Down Expand Up @@ -58,3 +65,13 @@ func RunSuites(t *testing.T, e html.ScriptEngine) {
t.Run("html", func(t *testing.T) { htmlsuite.RunHtmlSuite(t, e) })
t.Run("dom", func(t *testing.T) { domsuite.RunDomSuite(t, e) })
}

type dummyContext struct {
*entity.Entity
ctx context.Context
}

func (c dummyContext) Context() context.Context { return c.ctx }
func (c dummyContext) HTTPClient() http.Client { return *http.DefaultClient }
func (c dummyContext) LocationHREF() string { return "http://example.com" }
func (c dummyContext) Logger() *slog.Logger { return nil }
Comment on lines +69 to +77
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

rg -nC3 '\.Logger\(\)' --type=go | head -100

Repository: gost-dom/browser

Length of output: 5411


🏁 Script executed:

rg -nC3 'dummyContext' --type=go

Repository: gost-dom/browser

Length of output: 2982


🏁 Script executed:

rg -nC5 'func.*NewContext' --type=go | head -60

Repository: gost-dom/browser

Length of output: 2232


🏁 Script executed:

rg -n 'console\.' scripting/internal/scripttests/ --type=go | head -20

Repository: gost-dom/browser

Length of output: 167


Return a no-op logger instead of nil from Logger().

Logger() returns nil, which will cause panics when test scripts invoke console logging methods (e.g., console.log()). The script host's consoleAPIMessage() function calls host.Logger().Debug/Info/Warn/Error directly without nil checks.

🛡️ Proposed fix using slog's default or discard handler
-func (c dummyContext) Logger() *slog.Logger     { return nil }
+func (c dummyContext) Logger() *slog.Logger     { return slog.New(slog.DiscardHandler) }

Alternatively, if slog.DiscardHandler is not available in your Go version:

func (c dummyContext) Logger() *slog.Logger { return slog.Default() }

31 changes: 31 additions & 0 deletions scripting/sobekengine/array.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package sobekengine

import (
"errors"

"github.com/grafana/sobek"
)

type array struct {
value
obj *sobek.Object
}

func newArray(c *scriptContext, o *sobek.Object) jsArray {
return array{value{c, o}, o}
}

func (a array) Push(v jsValue) error {
push := a.obj.Get("push")
if push == nil {
return errors.New("gost-dom/sobekengine: array.push: Underlying object doesn't have a push")
}
p, ok := sobek.AssertFunction(push)
if !ok {
return errors.New(
"gost-dom/sobekengine: array.push: Underlying object's push is not a function",
)
}
_, err := p(a.obj, v.Self().value)
return err
}
Comment on lines +18 to +31
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Guard against nil value in Push to prevent panic.

If v is nil, calling v.Self() at line 29 will panic. Based on learnings, js.Value[T] can be nil even though it represents a JavaScript value.

🛡️ Proposed fix
 func (a array) Push(v jsValue) error {
+	if v == nil {
+		return errors.New("gost-dom/sobekengine: array.push: Cannot push nil value")
+	}
 	push := a.obj.Get("push")
 	if push == nil {
 		return errors.New("gost-dom/sobekengine: array.push: Underlying object doesn't have a push")

Alternatively, if pushing undefined is the intended behavior for nil:

 func (a array) Push(v jsValue) error {
 	push := a.obj.Get("push")
 	if push == nil {
 		return errors.New("gost-dom/sobekengine: array.push: Underlying object doesn't have a push")
 	}
 	p, ok := sobek.AssertFunction(push)
 	if !ok {
 		return errors.New(
 			"gost-dom/sobekengine: array.push: Underlying object's push is not a function",
 		)
 	}
-	_, err := p(a.obj, v.Self().value)
+	var arg sobek.Value
+	if v != nil {
+		arg = v.Self().value
+	}
+	_, err := p(a.obj, arg)
 	return err
 }

Based on learnings: In the gost-dom JavaScript binding system, js.Value[T] is a Go interface that can be nil even though it represents a JavaScript value. Always check v != nil before calling methods.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func (a array) Push(v jsValue) error {
push := a.obj.Get("push")
if push == nil {
return errors.New("gost-dom/sobekengine: array.push: Underlying object doesn't have a push")
}
p, ok := sobek.AssertFunction(push)
if !ok {
return errors.New(
"gost-dom/sobekengine: array.push: Underlying object's push is not a function",
)
}
_, err := p(a.obj, v.Self().value)
return err
}
func (a array) Push(v jsValue) error {
if v == nil {
return errors.New("gost-dom/sobekengine: array.push: Cannot push nil value")
}
push := a.obj.Get("push")
if push == nil {
return errors.New("gost-dom/sobekengine: array.push: Underlying object doesn't have a push")
}
p, ok := sobek.AssertFunction(push)
if !ok {
return errors.New(
"gost-dom/sobekengine: array.push: Underlying object's push is not a function",
)
}
_, err := p(a.obj, v.Self().value)
return err
}

Loading
Loading