import std/os import std/posix import std/strutils import std/unittest import monoucha/fromjs import monoucha/javascript import monoucha/optshim import monoucha/tojs test "Hello, world": let rt = newJSRuntime() let ctx = rt.newJSContext() const code = "'Hello from JS!'" let val = ctx.eval(code) var res: string check ctx.fromJS(val, res).isSome check res == "Hello from JS!" JS_FreeValue(ctx, val) ctx.free() rt.free() proc evalConvert[T](ctx: JSContext; code: string; file = ""): Result[T, string] = let val = ctx.eval(code, file) defer: JS_FreeValue(ctx, val) # unref result before returning var res: T if ctx.fromJS(val, res).isNone: # Conversion failed; return the exception message. return err(ctx.getExceptionMsg()) # All ok! Return the converted object. return ok(res) test "Error handling": let rt = newJSRuntime() let ctx = rt.newJSContext() const code = "abcd" let res = ctx.eval(code, "") check JS_IsException(res) const ex = """ ReferenceError: abcd is not defined at (:1:1) """ check ctx.getExceptionMsg() == ex check evalConvert[string](ctx, code, "").error == ex JS_FreeValue(ctx, res) ctx.free() rt.free() type Planet = ref object of RootObj Earth = ref object of Planet Moon = ref object of Planet proc jsAssert(earth: Earth; pred: bool) {.jsfunc: "assert".} = assert pred test "registerType: registering type interfaces": type Moon = ref object jsDestructor(Moon) let rt = newJSRuntime() let ctx = rt.newJSContext() ctx.registerType(Moon) const code = "Moon" let val = ctx.eval(code) var res: string check ctx.fromJS(val, res).isSome check res == """ function Moon() { [native code] }""" JS_FreeValue(ctx, val) ctx.free() rt.free() test "Global objects": let rt = newJSRuntime() let ctx = rt.newJSContext() let earth = Earth() ctx.registerType(Earth, asglobal = true) ctx.setGlobal(earth) const code = "assert(globalThis instanceof Earth)" let val = ctx.eval(code) check not JS_IsException(val) JS_FreeValue(ctx, val) ctx.free() rt.free() test "Inheritance": jsDestructor(Moon) jsDestructor(Planet) let rt = newJSRuntime() let ctx = rt.newJSContext() let planetCID = ctx.registerType(Planet) ctx.registerType(Earth, parent = planetCID, asglobal = true) ctx.registerType(Moon, parent = planetCID) ctx.setGlobal(Earth()) const code = "assert(globalThis instanceof Planet)" let val = ctx.eval(code) check not JS_IsException(val) JS_FreeValue(ctx, val) ctx.free() rt.free() test "jsget, jsset: basic property reflectors": type Moon = ref object Earth = ref object moon {.jsget.}: Moon name {.jsgetset.}: string population {.jsset.}: int64 jsDestructor(Moon) let rt = newJSRuntime() let ctx = rt.newJSContext() let earth = Earth(moon: Moon(), population: 1, name: "Earth") ctx.registerType(Earth, asglobal = true) ctx.registerType(Moon) ctx.setGlobal(earth) const code = """ globalThis.population = 8e9; "name: " + globalThis.name + ", moon: " + globalThis.moon; """ let val = ctx.eval(code) var res: string check ctx.fromJS(val, res).isSome check res == "name: Earth, moon: [object Moon]" check earth.population == int64(8e9) JS_FreeValue(ctx, val) ctx.free() rt.free() type Window = ref object console {.jsget.}: Console Console = ref object jsDestructor(Console) # aux stuff for tests proc jsAssert(window: Window; pred: bool) {.jsfunc: "assert".} = assert pred test "jsfunc: regular functions": proc log(console: Console; s: string) {.jsfunc.} = echo s let rt = newJSRuntime() let ctx = rt.newJSContext() let window = Window(console: Console()) ctx.registerType(Window, asglobal = true) ctx.registerType(Console) ctx.setGlobal(window) const code = """ console.log('Hello, world!') """ let val = ctx.eval(code) check not JS_IsException(val) JS_FreeValue(ctx, val) ctx.free() rt.free() type JSFile = ref object buffer: pointer # some internal buffer handled as managed memory path {.jsget.}: string jsDestructor(JSFile) proc newJSFile(path: string): JSFile {.jsctor.} = return JSFile( path: path, buffer: alloc(4096) ) test "jsctor: constructors": let rt = newJSRuntime() let ctx = rt.newJSContext() ctx.registerType(Window, asglobal = true) ctx.registerType(JSFile, name = "File") ctx.setGlobal(Window()) const code = """ assert(new File('/path/to/file') + '' == '[object File]') """ let val = ctx.eval(code) check not JS_IsException(val) JS_FreeValue(ctx, val) ctx.free() rt.free() func name(file: JSFile): string {.jsfget.} = return file.path.substr(file.path.rfind('/') + 1) proc setName(file: JSFile; s: string) {.jsfset: "name".} = let i = file.path.rfind('/') file.path = file.path.substr(0, i) & s test "jsfget, jsfset: custom property reflectors": let rt = newJSRuntime() let ctx = rt.newJSContext() ctx.registerType(Window, asglobal = true) ctx.registerType(JSFile, name = "File") ctx.setGlobal(Window()) const code = """ const file = new File("/path/to/file"); assert(file.path === "/path/to/file"); assert(file.name === "file"); /* file */ file.name = "new-name"; assert(file.path === "/path/to/new-name"); """ let val = ctx.eval(code) check not JS_IsException(val) JS_FreeValue(ctx, val) ctx.free() rt.free() proc jsExists(path: string): bool {.jsstfunc: "JSFile.exists".} = return fileExists(path) test "jsstfunc: static functions": let rt = newJSRuntime() let ctx = rt.newJSContext() ctx.registerType(Window, asglobal = true) ctx.registerType(JSFile, name = "File") ctx.setGlobal(Window()) const code = """ assert(File.exists("doc/manual.md")); """ let val = ctx.eval(code) check not JS_IsException(val) JS_FreeValue(ctx, val) ctx.free() rt.free() # this will always return the result of the fstat call. proc owner(file: JSFile): int {.jsuffget.} = let fd = open(cstring(file.path), O_RDONLY, 0) if fd == -1: return -1 var stats = Stat.default if fstat(fd, stats) == -1: discard close(fd) return -1 return int(stats.st_uid) proc getOwner(file: JSFile): int {.jsuffget.} = return file.owner test "jsuffunc, jsufget, jsuffget: the LegacyUnforgeable property": let rt = newJSRuntime() let ctx = rt.newJSContext() ctx.registerType(Window, asglobal = true) ctx.registerType(JSFile, name = "File") const code = """ const file = new File("doc/manual.md"); const oldGetOwner = file.getOwner; file.getOwner = () => -2; /* doesn't work */ assert(oldGetOwner == file.getOwner); Object.defineProperty(file, "owner", { value: -2 }); /* throws */ """ let val = ctx.eval(code) check JS_IsException(val) JS_FreeValue(ctx, val) ctx.free() rt.free() var unrefd {.global.} = 0 proc finalize(file: JSFile) {.jsfin.} = if file.buffer != nil: dealloc(file.buffer) # Note: it is not necessary to nil out the pointer; it's just me being # paranoid :P file.buffer = nil inc unrefd test "jsfin: object finalizers": let rt = newJSRuntime() let ctx = rt.newJSContext() GC_fullCollect() # ensure refc runs unrefd = 0 # ignore previous unrefs ctx.registerType(Window, asglobal = true) ctx.registerType(JSFile, name = "File") const code = """ /* this doesn't leak. yay :D */ { const file = new File("doc/manual.md"); } /* note that I put the above call in a separate scope, so QJS can unref * it immediately. in contrast, following file will not be deallocated until * the runtime is gone. */ const file = new File("doc/manual.md"); """ JS_FreeValue(ctx, ctx.eval(code)) GC_fullCollect() # ensure refc runs check unrefd == 1 # first file is already deallocated ctx.free() GC_fullCollect() # ensure refc runs check unrefd == 1 # the second file is still available rt.free() check unrefd == 2 # runtime is freed, so the second file gets deallocated too