package jscriptx_test import ( "context" "errors" "strings" "testing" "git.fsdpf.net/go/jscriptx" ) // ctorSpy 记录 constructor 有没有被跑过。 type ctorSpy struct{ built, startups int } func (s *ctorSpy) Name() string { return "spy" } func (s *ctorSpy) Module() (string, string) { return "", "" } func (s *ctorSpy) Bindings() map[string]any { return map[string]any{ "built": func() { s.built++ }, "startup": func() { s.startups++ }, } } const staticScript = ` export default class C { constructor() { spy.built() } static Startup() { spy.startup(); return "started" } Execute() { return "executed" } }` func newStaticScript(t *testing.T) (*jscriptx.Engine, *jscriptx.Script, *ctorSpy, context.Context) { t.Helper() e, err := jscriptx.New(jscriptx.WithLogger(nil)) if err != nil { t.Fatal(err) } t.Cleanup(e.Close) s, err := e.Compile("c.ts", staticScript) if err != nil { t.Fatal(err) } spy := &ctorSpy{} ctx := jscriptx.WithScope(context.Background(), jscriptx.ScopeExtensions(spy)) return e, s, spy, ctx } // 静态方法调得到,而且**不跑 constructor**。 func TestStatic_调静态方法不构造实例(t *testing.T) { _, s, spy, ctx := newStaticScript(t) got, err := s.CallStatic(ctx, "Startup") if err != nil { t.Fatal(err) } if got != "started" { t.Errorf("返回值 %#v", got) } if spy.startups != 1 { t.Errorf("Startup 该跑一次,跑了 %d 次", spy.startups) } if spy.built != 0 { t.Errorf("不该构造实例,constructor 却跑了 %d 次", spy.built) } } // 静态方法里能用作用域扩展——这正是它的用武之地(配置 Go 侧的东西)。 func TestStatic_能用作用域扩展(t *testing.T) { e, err := jscriptx.New(jscriptx.WithLogger(nil)) if err != nil { t.Fatal(err) } defer e.Close() s, err := e.Compile("c.ts", ` export default class C { static Startup() { return spy.tag() } }`) if err != nil { t.Fatal(err) } ext := tagExt{"来自扩展"} ctx := jscriptx.WithScope(context.Background(), jscriptx.ScopeExtensions(ext)) got, err := s.CallStatic(ctx, "Startup") if err != nil { t.Fatal(err) } if got != "来自扩展" { t.Errorf("拿到 %#v", got) } } type tagExt struct{ v string } func (tagExt) Name() string { return "spy" } func (tagExt) Module() (string, string) { return "", "" } func (e tagExt) Bindings() map[string]any { return map[string]any{"tag": func() string { return e.v }} } // 没写这个静态方法时要能被 ErrFuncNotFound 认出来——生命周期钩子多半是可选的。 func TestStatic_没写时报ErrFuncNotFound(t *testing.T) { _, s, _, ctx := newStaticScript(t) _, err := s.CallStatic(ctx, "Shutdown") if !errors.Is(err, jscriptx.ErrFuncNotFound) { t.Fatalf("该能被 ErrFuncNotFound 匹配上: %v", err) } } // 实例方法不算静态方法,别把两者搞混。 func TestStatic_实例方法不算静态(t *testing.T) { _, s, spy, ctx := newStaticScript(t) _, err := s.CallStatic(ctx, "Execute") if !errors.Is(err, jscriptx.ErrFuncNotFound) { t.Fatalf("Execute 是实例方法,不该被静态调用找到: %v", err) } if spy.built != 0 { t.Error("找不到也不该构造实例") } } // 静态方法抛异常时要把错误报出来,不能跟"没写"混为一谈。 func TestStatic_抛异常时报错(t *testing.T) { e, err := jscriptx.New(jscriptx.WithLogger(nil)) if err != nil { t.Fatal(err) } defer e.Close() s, err := e.Compile("c.ts", ` export default class C { static Startup() { throw new Error("配置缺失") } }`) if err != nil { t.Fatal(err) } _, err = s.CallStatic(context.Background(), "Startup") if err == nil { t.Fatal("该报错") } if errors.Is(err, jscriptx.ErrFuncNotFound) { t.Error("这是执行出错,不是没写") } if !strings.Contains(err.Error(), "配置缺失") { t.Errorf("错误里该带上脚本抛的内容: %v", err) } } // 静态方法建的 JS 对象活不下来:VM 用完就丢,后续调用看不到。 func TestStatic_建的对象活不下来(t *testing.T) { e, err := jscriptx.New(jscriptx.WithLogger(nil)) if err != nil { t.Fatal(err) } defer e.Close() s, err := e.Compile("c.ts", ` let prepared = "没跑过 Startup" export default class C { static Startup() { prepared = "Startup 跑过了"; return prepared } Read() { return prepared } }`) if err != nil { t.Fatal(err) } ctx := context.Background() if got, err := s.CallStatic(ctx, "Startup"); err != nil || got != "Startup 跑过了" { t.Fatalf("got=%#v err=%v", got, err) } // 换一个 VM 读,看不到 Startup 改的东西 obj, err := s.New(ctx) if err != nil { t.Fatal(err) } defer obj.Close() got, err := obj.Call(ctx, "Read") if err != nil { t.Fatal(err) } if got != "没跑过 Startup" { t.Errorf("静态方法改的顶层变量不该跨 VM 存活,读到 %#v", got) } } // HasStatic 只认静态方法。 func TestStatic_HasStatic(t *testing.T) { _, s, spy, _ := newStaticScript(t) if !s.HasStatic("Startup") { t.Error("Startup 是静态方法,该认出来") } if s.HasStatic("Execute") { t.Error("Execute 是实例方法,不该算静态") } if s.HasStatic("Nope") { t.Error("不存在的名字不该算") } if spy.built != 0 { t.Error("HasStatic 不该构造实例") } } // 脚本导出的不是 class 时,静态调用要老实说找不到。 func TestStatic_非class脚本(t *testing.T) { e, err := jscriptx.New(jscriptx.WithLogger(nil)) if err != nil { t.Fatal(err) } defer e.Close() s, err := e.Compile("c.ts", `export default function () { return 1 }`) if err != nil { t.Fatal(err) } if s.HasStatic("Startup") { t.Error("函数导出没有静态方法") } if _, err := s.CallStatic(context.Background(), "Startup"); !errors.Is(err, jscriptx.ErrFuncNotFound) { t.Errorf("该报 ErrFuncNotFound: %v", err) } }