Files
jscriptx/bundle_test.go
T
what 7f484d7a71 test: 引擎核心的测试与基准
除了常规用例,有几组是专门钉住「引擎的既定事实」的,改动时会先红:

  - vmstate_test  goja 的硬限制:非原始值不是 goroutine 安全的、对象跨不了
                  Runtime。其中一条是哨兵——goja 哪天允许对象跨 Runtime 了,
                  它会失败,提醒我们可以简化设计。
  - govalue_test  Go 的切片/map 进到脚本里长什么样。这不是本库的行为而是
                  goja 的,但脚本作者照着它写代码,变了会静默走错分支。
  - safety_test   并发下坏 VM 不会被别的 goroutine 捡到。
  - bench_gobind  脚本碰 Go 对象的单次开销,README 性能一节的数据来源。

missing_global_test 里有一条断言 typeof setTimeout === "undefined":
定时器必须保持未定义,库里的特性探测才能正常降级,防止以后有人把桩加回来。
2026-09-05 22:13:57 +08:00

159 lines
4.6 KiB
Go

package jscriptx_test
import (
"context"
"errors"
"path/filepath"
"strings"
"testing"
"git.fsdpf.net/go/jscriptx"
"github.com/evanw/esbuild/pkg/api"
)
// WithBundleDefine:编译期常量替换。
func TestBundleOption_Define(t *testing.T) {
e := newEngine(t, jscriptx.WithBundleOptions(
jscriptx.WithBundleDefine(map[string]string{"__MODE__": `"生产"`}),
))
s := mustCompile(t, e, "def.ts", `
declare const __MODE__: string
export function mode(): string { return __MODE__ }
`)
got, err := s.Call(context.Background(), "mode")
if err != nil {
t.Fatal(err)
}
if got != "生产" {
t.Errorf("常量没被替换, got %#v", got)
}
}
// WithBundleTarget:把语法降级到指定的 ECMAScript 版本。
// goja 对新语法的覆盖不是 100%,调低 target 是遇到问题时的出路。
func TestBundleOption_Target(t *testing.T) {
// 用可选链和空值合并这类较新的语法,降级到 ES2015 后应该被改写掉
src := `export function pick(o) { return o?.a?.b ?? "兜底" }`
for _, target := range []struct {
name string
t api.Target
}{{"ES2015", api.ES2015}, {"ES2020", api.ES2020}} {
t.Run(target.name, func(t *testing.T) {
e := newEngine(t, jscriptx.WithBundleOptions(jscriptx.WithBundleTarget(target.t)))
s := mustCompile(t, e, "t.ts", src)
ctx := context.Background()
got, err := s.Call(ctx, "pick", map[string]any{"a": map[string]any{"b": "命中"}})
if err != nil {
t.Fatalf("调用失败: %v", err)
}
if got != "命中" {
t.Errorf("got = %#v", got)
}
if got, err := s.Call(ctx, "pick", nil); err != nil || got != "兜底" {
t.Errorf("空值合并没生效: got=%#v err=%v", got, err)
}
})
}
}
// WithNodePaths / WithAlias:给直接 Compile 的源码指定模块来源。
func TestBundleOption_NodePathsAndAlias(t *testing.T) {
libs, err := filepath.Abs("esm/testdata/app/node_modules")
if err != nil {
t.Fatal(err)
}
src := `import { upper } from "tinylib"
export function shout(s: string): string { return upper(s) }`
// 不指定时找不到:这段源码没有文件系统上下文
e0 := newEngine(t)
if _, err := e0.Compile("x.ts", src); err == nil {
t.Error("没有解析基准时应该报错")
}
// NodePaths 指向 node_modules 所在目录
e1 := newEngine(t, jscriptx.WithBundleOptions(jscriptx.WithNodePaths(libs)))
s1, err := e1.Compile("x.ts", src)
if err != nil {
t.Fatalf("WithNodePaths 没生效: %v", err)
}
if got, _ := s1.Call(context.Background(), "shout", "hi"); got != "HI" {
t.Errorf("got = %#v", got)
}
// Alias 把模块名钉到具体文件
impl, err := filepath.Abs("esm/testdata/app/node_modules/tinylib/index.js")
if err != nil {
t.Fatal(err)
}
e2 := newEngine(t, jscriptx.WithBundleOptions(
jscriptx.WithAlias(map[string]string{"tinylib": impl}),
))
s2, err := e2.Compile("x.ts", src)
if err != nil {
t.Fatalf("WithAlias 没生效: %v", err)
}
if got, _ := s2.Call(context.Background(), "shout", "hi"); got != "HI" {
t.Errorf("got = %#v", got)
}
}
// 直接 Compile 的源码不能有 import,报错要指出走 esm 子包。
func TestEntry_Compile不支持import(t *testing.T) {
e := newEngine(t)
_, err := e.Compile("imp.ts", `import x from "./other"
export default class H { m() { return x } }`)
if err == nil {
t.Fatal("应该报错")
}
for _, want := range []string{"Could not resolve", "jscriptx/esm"} {
if !strings.Contains(err.Error(), want) {
t.Errorf("报错里没有 %q: %v", want, err)
}
}
t.Logf("%v", err)
}
// 配了 WithResolveDir 之后,直接 Compile 的源码也能 import。
func TestEntry_ResolveDir(t *testing.T) {
e := newEngine(t, jscriptx.WithBundleOptions(
jscriptx.WithResolveDir("esm/testdata/app/src/Resource"),
))
s, err := e.Compile("inline.ts", `import ResExecuter from "./ResExecuter"
export default class H {
m() { return new ResExecuter().Store("cfg") }
}`)
if err != nil {
t.Fatalf("配了 resolveDir 应该能 import: %v", err)
}
ctx := context.Background()
obj, err := s.New(ctx)
if err != nil {
t.Fatal(err)
}
defer obj.Close()
if got, _ := obj.Call(ctx, "m"); got != "stored:cfg" {
t.Errorf("got = %#v", got)
}
}
// 语法错误在 Compile 阶段就报,带文件位置。
func TestEntry_语法错误(t *testing.T) {
e := newEngine(t)
_, err := e.Compile("bad.ts", `export default class { 语法错误 (((`)
if err == nil {
t.Fatal("应该报语法错误")
}
var jsErr *jscriptx.Error
if !errors.As(err, &jsErr) || jsErr.Kind != jscriptx.KindCompile {
t.Fatalf("错误分类不对: %v", err)
}
if !strings.Contains(err.Error(), "bad.ts") {
t.Errorf("报错没带文件名: %v", err)
}
t.Logf("%v", err)
}