package jscriptx_test import ( "context" "errors" "strings" "testing" "time" "git.fsdpf.net/go/jscriptx" ) // 所有脚本都要经 esbuild 打包,入口只有一条路:模块导出。 // 这里把各种导出形态过一遍。 func TestEntry_各种导出形态(t *testing.T) { cases := []struct { name string src string fn string args []any want any newObj bool // 需要先 New 出实例(导出的是 class) }{ { name: "export default class", src: `export default class DeviceHandler { constructor(id) { this.id = id } info() { return "class:" + this.id } }`, fn: "info", newObj: true, args: []any{"d1"}, want: "class:d1", }, { name: "export default 匿名 class", src: `export default class { constructor(id) { this.id = id } info() { return "匿名:" + this.id } }`, fn: "info", newObj: true, args: []any{"d2"}, want: "匿名:d2", }, { name: "export default 实例", src: `class H { info() { return "实例" } } export default new H()`, fn: "info", want: "实例", }, { name: "export default 对象字面量", src: `export default { info: function () { return "对象" } }`, fn: "info", want: "对象", }, { name: "export default 具名函数", src: `export default function handler(x) { return x * 2 }`, fn: jscriptx.DefaultFunc, args: []any{21}, want: int64(42), }, { name: "export default 箭头函数", src: `export default (x) => x + 1`, fn: jscriptx.DefaultFunc, args: []any{41}, want: int64(42), }, { name: "只有命名导出", src: `export function Options() { return "opts" } export function Version() { return "v1" }`, fn: "Options", want: "opts", }, { name: "继承来的方法也找得到", src: `class Base { base() { return "base" } } export default class Derived extends Base { own() { return "own" } }`, fn: "base", newObj: true, want: "base", }, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { e := newEngine(t) s := mustCompile(t, e, "mod.ts", c.src) ctx := context.Background() var got any var err error if c.newObj { obj, e2 := s.New(ctx, c.args...) if e2 != nil { t.Fatalf("New 失败: %v", e2) } defer obj.Close() got, err = obj.Call(ctx, c.fn) } else { got, err = s.Call(ctx, c.fn, c.args...) } if err != nil { t.Fatalf("调用失败: %v", err) } if got != c.want { t.Errorf("got = %#v, want %#v", got, c.want) } }) } } // TypeScript 直接能写。 func TestEntry_TypeScript(t *testing.T) { e := newEngine(t) s := mustCompile(t, e, "ts.ts", ` interface Cfg { name: string } export default class C { private count: number = 0 constructor(private cfg: Cfg) {} Bump(): number { return ++this.count } Name(): string { return this.cfg.name } } `) ctx := context.Background() obj, err := s.New(ctx, map[string]any{"name": "配置A"}) if err != nil { t.Fatal(err) } defer obj.Close() if got, _ := obj.Call(ctx, "Name"); got != "配置A" { t.Errorf("Name = %#v", got) } if got, _ := obj.Call(ctx, "Bump"); got.(int64) != 1 { t.Errorf("Bump = %#v", got) } } // 不写 export 的脚本会被打包器摇空,报错要说清楚这一点。 func TestEntry_没有导出时报错有指引(t *testing.T) { e := newEngine(t) s := mustCompile(t, e, "noexport.js", `function handle(x) { return x * 2 }`) _, err := s.Call(context.Background(), "handle", 1) if !errors.Is(err, jscriptx.ErrFuncNotFound) { t.Fatalf("want ErrFuncNotFound, got %v", err) } var jsErr *jscriptx.Error errors.As(err, &jsErr) for _, want := range []string{"没有任何导出", "export"} { if !strings.Contains(jsErr.Msg, want) { t.Errorf("报错里没有 %q: %s", want, jsErr.Msg) } } t.Logf("%s", jsErr.Msg) } // constructor 抛异常要报清楚是构造阶段失败。 func TestEntry_构造失败(t *testing.T) { e := newEngine(t) s := mustCompile(t, e, "bad.ts", ` export default class H { constructor(cfg) { if (!cfg) { throw new Error("缺少配置") } this.cfg = cfg } run() { return this.cfg } } `) _, err := s.New(context.Background()) // 故意不传构造参数 if err == nil { t.Fatal("构造失败应该报错") } var jsErr *jscriptx.Error if !errors.As(err, &jsErr) { t.Fatalf("错误类型不对: %T", err) } if jsErr.Func != "constructor" { t.Errorf("应该指明是 constructor 出错: %+v", jsErr) } if !strings.Contains(jsErr.Msg, "缺少配置") { t.Errorf("没带上脚本的错误信息: %q", jsErr.Msg) } } // 池化调用下,实例状态 = VM 状态,换了 VM 就归零,只能当缓存。 func TestEntry_池化下实例状态会丢(t *testing.T) { e := newEngine(t, jscriptx.WithTimeout(80*time.Millisecond)) s := mustCompile(t, e, "counter.ts", ` export default class Counter { constructor() { this.count = 0 } bump() { return ++this.count } spin() { while (true) {} } } `) ctx := context.Background() if got, _ := s.Call(ctx, "bump"); got.(int64) != 1 { t.Fatalf("got %#v", got) } if got, _ := s.Call(ctx, "bump"); got.(int64) != 2 { t.Fatalf("同一个 VM 内应该累加, got %#v", got) } if _, err := s.Call(ctx, "spin"); !errors.Is(err, jscriptx.ErrTimeout) { t.Fatalf("want ErrTimeout, got %v", err) } got, err := s.Call(ctx, "bump") if err != nil { t.Fatal(err) } if got.(int64) != 1 { t.Errorf("池化模式换了 VM,实例状态应该归零,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) }