[gjs/ewlsh/register-type] Implement GObject.registerType API
- From: Evan Welsh <ewlsh src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gjs/ewlsh/register-type] Implement GObject.registerType API
- Date: Sun, 12 Sep 2021 07:22:37 +0000 (UTC)
commit 6d21f47ab752a02989207024a2a0877d31ae9627
Author: Evan Welsh <contact evanwelsh com>
Date: Sun Sep 12 00:13:01 2021 -0700
Implement GObject.registerType API
GObject.registerType is a complementary API to
GObject.registerClass which is compatible with
new class features like class fields.
GObject.registerType does not replace the
original constructor, instead it stores the
GObject prototype separately.
Fixes #331
gi/boxed.cpp | 7 +
gi/boxed.h | 1 +
gi/fundamental.cpp | 6 +
gi/fundamental.h | 2 +
gi/gerror.cpp | 5 +
gi/gerror.h | 1 +
gi/object.cpp | 13 +-
gi/object.h | 3 +-
gi/private.cpp | 95 +-
gi/union.cpp | 5 +
gi/union.h | 1 +
gi/wrapperutils.h | 131 ++-
gjs/atoms.h | 1 +
installed-tests/js/meson.build | 1 +
installed-tests/js/testGObjectType.js | 1712 +++++++++++++++++++++++++++++++++
modules/core/overrides/GObject.js | 247 ++++-
16 files changed, 2154 insertions(+), 77 deletions(-)
---
diff --git a/gi/boxed.cpp b/gi/boxed.cpp
index 7327c317..b12ce6ad 100644
--- a/gi/boxed.cpp
+++ b/gi/boxed.cpp
@@ -38,6 +38,13 @@
#include "gjs/mem-private.h"
#include "util/log.h"
+BoxedInstance::BoxedInstance(BoxedPrototype* prototype, JS::HandleObject obj)
+ : GIWrapperInstance(prototype, obj),
+ m_allocated_directly(false),
+ m_owning_ptr(false) {
+ GJS_INC_COUNTER(boxed_instance);
+}
+
BoxedInstance::BoxedInstance(JSContext* cx, JS::HandleObject obj)
: GIWrapperInstance(cx, obj),
m_allocated_directly(false),
diff --git a/gi/boxed.h b/gi/boxed.h
index 7c1cd49c..5c882c4b 100644
--- a/gi/boxed.h
+++ b/gi/boxed.h
@@ -160,6 +160,7 @@ class BoxedInstance
bool m_owning_ptr : 1; // if set, the JS wrapper owns the C memory referred
// to by m_ptr.
+ explicit BoxedInstance(BoxedPrototype* prototype, JS::HandleObject obj);
explicit BoxedInstance(JSContext* cx, JS::HandleObject obj);
~BoxedInstance(void);
diff --git a/gi/fundamental.cpp b/gi/fundamental.cpp
index 72b69259..0fcc4b9f 100644
--- a/gi/fundamental.cpp
+++ b/gi/fundamental.cpp
@@ -33,6 +33,12 @@ namespace JS {
class CallArgs;
}
+FundamentalInstance::FundamentalInstance(FundamentalPrototype* prototype,
+ JS::HandleObject obj)
+ : GIWrapperInstance(prototype, obj) {
+ GJS_INC_COUNTER(fundamental_instance);
+}
+
FundamentalInstance::FundamentalInstance(JSContext* cx, JS::HandleObject obj)
: GIWrapperInstance(cx, obj) {
GJS_INC_COUNTER(fundamental_instance);
diff --git a/gi/fundamental.h b/gi/fundamental.h
index 7d2f8de0..afcbbac7 100644
--- a/gi/fundamental.h
+++ b/gi/fundamental.h
@@ -144,6 +144,8 @@ class FundamentalInstance
friend class GIWrapperBase<FundamentalBase, FundamentalPrototype,
FundamentalInstance>;
+ explicit FundamentalInstance(FundamentalPrototype* prototype,
+ JS::HandleObject obj);
explicit FundamentalInstance(JSContext* cx, JS::HandleObject obj);
~FundamentalInstance(void);
diff --git a/gi/gerror.cpp b/gi/gerror.cpp
index 08745a59..aa481abd 100644
--- a/gi/gerror.cpp
+++ b/gi/gerror.cpp
@@ -48,6 +48,11 @@ ErrorInstance::ErrorInstance(JSContext* cx, JS::HandleObject obj)
GJS_INC_COUNTER(gerror_instance);
}
+ErrorInstance::ErrorInstance(ErrorPrototype* prototype, JS::HandleObject obj)
+ : GIWrapperInstance(prototype, obj) {
+ GJS_INC_COUNTER(gerror_instance);
+}
+
ErrorInstance::~ErrorInstance(void) {
GJS_DEC_COUNTER(gerror_instance);
}
diff --git a/gi/gerror.h b/gi/gerror.h
index 8841a994..d225c1bc 100644
--- a/gi/gerror.h
+++ b/gi/gerror.h
@@ -128,6 +128,7 @@ class ErrorInstance : public GIWrapperInstance<ErrorBase, ErrorPrototype,
GError>;
friend class GIWrapperBase<ErrorBase, ErrorPrototype, ErrorInstance>;
+ explicit ErrorInstance(ErrorPrototype* prototype, JS::HandleObject obj);
explicit ErrorInstance(JSContext* cx, JS::HandleObject obj);
~ErrorInstance(void);
diff --git a/gi/object.cpp b/gi/object.cpp
index 6b5ddfdf..8711bc10 100644
--- a/gi/object.cpp
+++ b/gi/object.cpp
@@ -1370,8 +1370,9 @@ void ObjectInstance::prepare_shutdown(void) {
std::mem_fn(&ObjectInstance::release_native_object));
}
-ObjectInstance::ObjectInstance(JSContext* cx, JS::HandleObject object)
- : GIWrapperInstance(cx, object),
+ObjectInstance::ObjectInstance(ObjectPrototype* prototype,
+ JS::HandleObject object)
+ : GIWrapperInstance(prototype, object),
m_wrapper_finalized(false),
m_gobj_disposed(false),
m_gobj_finalized(false),
@@ -2486,11 +2487,15 @@ ObjectInstance* ObjectInstance::new_for_gobject(JSContext* cx, GObject* gobj) {
return nullptr;
JS::RootedObject obj(
- cx, JS_NewObjectWithGivenProto(cx, JS_GetClass(proto), proto));
+ cx, JS_NewObjectWithGivenProto(cx, &ObjectBase::klass, proto));
if (!obj)
return nullptr;
- ObjectInstance* priv = ObjectInstance::new_for_js_object(cx, obj);
+ ObjectPrototype* prototype = resolve_prototype(cx, proto);
+ if (!prototype)
+ return nullptr;
+
+ ObjectInstance* priv = ObjectInstance::new_for_js_object(prototype, obj);
g_object_ref_sink(gobj);
priv->associate_js_gobject(cx, obj, gobj);
diff --git a/gi/object.h b/gi/object.h
index e988189c..8827255d 100644
--- a/gi/object.h
+++ b/gi/object.h
@@ -302,7 +302,8 @@ class ObjectInstance : public GIWrapperInstance<ObjectBase, ObjectPrototype,
/* Constructors */
private:
- ObjectInstance(JSContext* cx, JS::HandleObject obj);
+ ObjectInstance(JSContext* cx, JS::HandleObject obj) = delete;
+ ObjectInstance(ObjectPrototype* prototype, JS::HandleObject obj);
~ObjectInstance();
GJS_JSAPI_RETURN_CONVENTION
diff --git a/gi/private.cpp b/gi/private.cpp
index a6ecc723..368d873e 100644
--- a/gi/private.cpp
+++ b/gi/private.cpp
@@ -17,6 +17,7 @@
#include <js/RootingAPI.h>
#include <js/TypeDecls.h>
#include <js/Utility.h> // for UniqueChars
+#include <js/ValueArray.h>
#include <jsapi.h> // for JS_GetElement
#include "gi/gobject.h"
@@ -240,18 +241,11 @@ static inline void gjs_add_interface(GType instance_type,
}
GJS_JSAPI_RETURN_CONVENTION
-static bool gjs_register_type(JSContext* cx, unsigned argc, JS::Value* vp) {
- JS::CallArgs argv = JS::CallArgsFromVp(argc, vp);
-
- JS::UniqueChars name;
- GTypeFlags type_flags;
- JS::RootedObject parent(cx), interfaces(cx), properties(cx);
- if (!gjs_parse_call_args(cx, "register_type", argv, "osioo", "parent",
- &parent, "name", &name, "flags", &type_flags,
- "interfaces", &interfaces,
- "properties", &properties))
- return false;
-
+static bool gjs_register_type_impl(JSContext* cx, const char* name,
+ GTypeFlags type_flags,
+ JS::HandleObject parent,
+ JS::HandleObject interfaces,
+ JS::HandleObject properties, GType* gtype) {
if (!parent)
return false;
@@ -273,8 +267,8 @@ static bool gjs_register_type(JSContext* cx, unsigned argc, JS::Value* vp) {
if (!get_interface_gtypes(cx, interfaces, n_interfaces, iface_types))
return false;
- if (g_type_from_name(name.get()) != G_TYPE_INVALID) {
- gjs_throw(cx, "Type name %s is already registered", name.get());
+ if (g_type_from_name(name) != G_TYPE_INVALID) {
+ gjs_throw(cx, "Type name %s is already registered", name);
return false;
}
@@ -293,8 +287,8 @@ static bool gjs_register_type(JSContext* cx, unsigned argc, JS::Value* vp) {
type_info.class_size = query.class_size;
type_info.instance_size = query.instance_size;
- GType instance_type = g_type_register_static(
- parent_priv->gtype(), name.get(), &type_info, type_flags);
+ GType instance_type = g_type_register_static(parent_priv->gtype(), name,
+ &type_info, type_flags);
g_type_set_qdata(instance_type, ObjectBase::custom_type_quark(),
GINT_TO_POINTER(1));
@@ -306,6 +300,28 @@ static bool gjs_register_type(JSContext* cx, unsigned argc, JS::Value* vp) {
for (uint32_t ix = 0; ix < n_interfaces; ix++)
gjs_add_interface(instance_type, iface_types[ix]);
+ *gtype = instance_type;
+ return true;
+}
+
+GJS_JSAPI_RETURN_CONVENTION
+static bool gjs_register_type(JSContext* cx, unsigned argc, JS::Value* vp) {
+ JS::CallArgs argv = JS::CallArgsFromVp(argc, vp);
+
+ JS::UniqueChars name;
+ GTypeFlags type_flags;
+ JS::RootedObject parent(cx), interfaces(cx), properties(cx);
+ if (!gjs_parse_call_args(cx, "register_type", argv, "osioo", "parent",
+ &parent, "name", &name, "flags", &type_flags,
+ "interfaces", &interfaces, "properties",
+ &properties))
+ return false;
+
+ GType instance_type;
+ if (!gjs_register_type_impl(cx, name.get(), type_flags, parent, interfaces,
+ properties, &instance_type))
+ return false;
+
/* create a custom JSClass */
JS::RootedObject module(cx, gjs_lookup_private_namespace(cx));
JS::RootedObject constructor(cx), prototype(cx);
@@ -321,6 +337,47 @@ static bool gjs_register_type(JSContext* cx, unsigned argc, JS::Value* vp) {
return true;
}
+GJS_JSAPI_RETURN_CONVENTION
+static bool gjs_register_type_with_class(JSContext* cx, unsigned argc,
+ JS::Value* vp) {
+ JS::CallArgs argv = JS::CallArgsFromVp(argc, vp);
+
+ JS::UniqueChars name;
+ GTypeFlags type_flags;
+ JS::RootedObject klass(cx), parent(cx), interfaces(cx), properties(cx);
+ if (!gjs_parse_call_args(cx, "register_type_with_class", argv, "oosioo",
+ "class", &klass, "parent", &parent, "name", &name,
+ "flags", &type_flags, "interfaces", &interfaces,
+ "properties", &properties))
+ return false;
+
+ GType instance_type;
+ if (!gjs_register_type_impl(cx, name.get(), type_flags, parent, interfaces,
+ properties, &instance_type))
+ return false;
+
+ /* create a custom JSClass */
+ JS::RootedObject module(cx, gjs_lookup_private_namespace(cx));
+ JS::RootedObject prototype(cx);
+
+ if (!ObjectPrototype::wrap_class(cx, module, nullptr, instance_type, klass,
+ &prototype))
+ return false;
+
+ auto* priv = ObjectPrototype::for_js(cx, prototype);
+ priv->set_type_qdata();
+
+ JS::RootedObject gtype_wrapper(
+ cx, gjs_gtype_create_gtype_wrapper(cx, instance_type));
+ JS::RootedValueArray<2> tuple(cx);
+ tuple[0].setObject(*prototype);
+ tuple[1].setObject(*gtype_wrapper);
+ JS::RootedObject array(cx, JS::NewArrayObject(cx, tuple));
+ argv.rval().setObject(*array);
+
+ return true;
+}
+
GJS_JSAPI_RETURN_CONVENTION
static bool gjs_signal_new(JSContext* cx, unsigned argc, JS::Value* vp) {
JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
@@ -408,11 +465,17 @@ static JSFunctionSpec private_module_funcs[] = {
JS_FN("register_interface", gjs_register_interface, 3,
GJS_MODULE_PROP_FLAGS),
JS_FN("register_type", gjs_register_type, 4, GJS_MODULE_PROP_FLAGS),
+ JS_FN("register_type_with_class", gjs_register_type_with_class, 5,
+ GJS_MODULE_PROP_FLAGS),
JS_FN("signal_new", gjs_signal_new, 6, GJS_MODULE_PROP_FLAGS),
JS_FS_END,
};
static JSPropertySpec private_module_props[] = {
+ JS_PSG("gobject_prototype_symbol",
+ symbol_getter<&GjsAtoms::gobject_prototype>, GJS_MODULE_PROP_FLAGS),
+ JS_PSG("gobject_type_symbol", symbol_getter<&GjsAtoms::gobject_type>,
+ GJS_MODULE_PROP_FLAGS),
JS_PSG("hook_up_vfunc_symbol", symbol_getter<&GjsAtoms::hook_up_vfunc>,
GJS_MODULE_PROP_FLAGS),
JS_PSG("signal_find_symbol", symbol_getter<&GjsAtoms::signal_find>,
diff --git a/gi/union.cpp b/gi/union.cpp
index a91225d4..4c3d2ec5 100644
--- a/gi/union.cpp
+++ b/gi/union.cpp
@@ -34,6 +34,11 @@ UnionInstance::UnionInstance(JSContext* cx, JS::HandleObject obj)
GJS_INC_COUNTER(union_instance);
}
+UnionInstance::UnionInstance(UnionPrototype* prototype, JS::HandleObject obj)
+ : GIWrapperInstance(prototype, obj) {
+ GJS_INC_COUNTER(union_instance);
+}
+
UnionInstance::~UnionInstance(void) {
if (m_ptr) {
g_boxed_free(gtype(), m_ptr);
diff --git a/gi/union.h b/gi/union.h
index 269cf156..1f4b1864 100644
--- a/gi/union.h
+++ b/gi/union.h
@@ -65,6 +65,7 @@ class UnionInstance
friend class GIWrapperInstance<UnionBase, UnionPrototype, UnionInstance>;
friend class GIWrapperBase<UnionBase, UnionPrototype, UnionInstance>;
+ explicit UnionInstance(UnionPrototype* prototype, JS::HandleObject obj);
explicit UnionInstance(JSContext* cx, JS::HandleObject obj);
~UnionInstance(void);
diff --git a/gi/wrapperutils.h b/gi/wrapperutils.h
index 777fb72c..8fb59338 100644
--- a/gi/wrapperutils.h
+++ b/gi/wrapperutils.h
@@ -10,6 +10,7 @@
#include <stdint.h>
+#include <new>
#include <string>
#include <girepository.h>
@@ -297,6 +298,43 @@ class GIWrapperBase : public CWrapperPointerOps<Base> {
}
protected:
+ /**
+ * GIWrapperBase::resolve_prototype:
+ */
+ [[nodiscard]] static Prototype* resolve_prototype(JSContext* cx,
+ JS::HandleObject proto) {
+ if (JS_GetClass(proto) == &Base::klass) {
+ return Prototype::for_js(cx, proto);
+ }
+
+ const GjsAtoms& atoms = GjsContextPrivate::atoms(cx);
+
+ bool has_property = false;
+ if (!JS_HasOwnPropertyById(cx, proto, atoms.gobject_prototype(),
+ &has_property))
+ return nullptr;
+
+ if (!has_property) {
+ gjs_throw(cx, "Tried to construct an object without a GType!");
+ return nullptr;
+ }
+
+ JS::RootedValue gobject_proto(cx);
+ if (!JS_GetPropertyById(cx, proto, atoms.gobject_prototype(),
+ &gobject_proto))
+ return nullptr;
+
+ if (!gobject_proto.isObject()) {
+ gjs_throw(cx, "Tried to construct an object without a GType!");
+ return nullptr;
+ }
+
+ JS::RootedObject obj(cx, &gobject_proto.toObject());
+ g_assert(JS_GetClass(obj) == &Base::klass);
+
+ return Prototype::for_js(cx, obj);
+ }
+
/*
* GIWrapperBase::resolve:
*
@@ -429,14 +467,14 @@ class GIWrapperBase : public CWrapperPointerOps<Base> {
JS::RootedObject proto(cx);
if (!JS_GetPrototype(cx, obj, &proto))
return false;
- if (JS_GetClass(proto) != &Base::klass) {
- gjs_throw(cx, "Tried to construct an object without a GType");
+
+ Prototype* prototype = resolve_prototype(cx, proto);
+ if (!prototype)
return false;
- }
args.rval().setUndefined();
- Instance* priv = Instance::new_for_js_object(cx, obj);
+ Instance* priv = Instance::new_for_js_object(prototype, obj);
{
std::string fullName = priv->format_name();
@@ -641,6 +679,9 @@ class GIWrapperBase : public CWrapperPointerOps<Base> {
template <class Base, class Prototype, class Instance,
typename Info = GIObjectInfo>
class GIWrapperPrototype : public Base {
+ using GjsAutoPrototype =
+ GjsAutoPointer<Prototype, void, g_atomic_rc_box_release>;
+
protected:
// m_info may be null in the case of JS-defined types, or internal types
// not exposed through introspection, such as GLocalFile. Not all subclasses
@@ -798,6 +839,22 @@ class GIWrapperPrototype : public Base {
cx, constructor, m_gtype, m_info);
}
+ GJS_JSAPI_RETURN_CONVENTION
+ static Prototype* create_prototype(Info* info, GType gtype) {
+ g_assert(gtype != G_TYPE_INVALID);
+
+ // We have to keep the Prototype in an arcbox because some of its
+ // members are needed in some Instance destructors, e.g. m_gtype to
+ // figure out how to free the Instance's m_ptr, and m_info to figure out
+ // how many bytes to free if it is allocated directly. Storing a
+ // refcount on the prototype is cheaper than storing pointers to m_info
+ // and m_gtype on each instance.
+ Prototype* priv = g_atomic_rc_box_new0(Prototype);
+ new (priv) Prototype(info, gtype);
+
+ return priv;
+ }
+
public:
/**
* GIWrapperPrototype::create_class:
@@ -828,17 +885,8 @@ class GIWrapperPrototype : public Base {
JS::MutableHandleObject constructor,
JS::MutableHandleObject prototype) {
g_assert(in_object);
- g_assert(gtype != G_TYPE_INVALID);
- // We have to keep the Prototype in an arcbox because some of its
- // members are needed in some Instance destructors, e.g. m_gtype to
- // figure out how to free the Instance's m_ptr, and m_info to figure out
- // how many bytes to free if it is allocated directly. Storing a
- // refcount on the prototype is cheaper than storing pointers to m_info
- // and m_gtype on each instance.
- GjsAutoPointer<Prototype, void, g_atomic_rc_box_release> priv =
- g_atomic_rc_box_new0(Prototype);
- new (priv) Prototype(info, gtype);
+ GjsAutoPrototype priv = create_prototype(info, gtype);
if (!priv->init(cx))
return nullptr;
@@ -873,6 +921,40 @@ class GIWrapperPrototype : public Base {
return proto;
}
+ GJS_JSAPI_RETURN_CONVENTION
+ static Prototype* wrap_class(JSContext* cx, JS::HandleObject in_object,
+ Info* info, GType gtype,
+ JS::HandleObject constructor,
+ JS::MutableHandleObject prototype) {
+ g_assert(in_object);
+
+ GjsAutoPrototype priv = create_prototype(info, gtype);
+ if (!priv->init(cx))
+ return nullptr;
+
+ JS::RootedObject parent_proto(cx);
+ if (!priv->get_parent_proto(cx, &parent_proto))
+ return nullptr;
+
+ prototype.set(
+ JS_NewObjectWithGivenProto(cx, &Base::klass, parent_proto));
+ if (!prototype)
+ return nullptr;
+
+ Prototype* proto = priv.release();
+ JS_SetPrivate(prototype, proto);
+
+ if (!proto->define_static_methods(cx, constructor))
+ return nullptr;
+
+ GjsAutoChar class_name = g_strdup_printf("%s", proto->name());
+ if (!JS_DefineProperty(cx, in_object, class_name, constructor,
+ GJS_MODULE_PROP_FLAGS))
+ return nullptr;
+
+ return proto;
+ }
+
// Methods to get an existing Prototype
/*
@@ -954,11 +1036,15 @@ class GIWrapperInstance : public Base {
protected:
GjsSmartPointer<Wrapped> m_ptr;
- explicit GIWrapperInstance(JSContext* cx, JS::HandleObject obj)
- : Base(Prototype::for_js_prototype(cx, obj)), m_ptr(nullptr) {
+ explicit GIWrapperInstance(Prototype* prototype, JS::HandleObject obj)
+ : Base(prototype), m_ptr(nullptr) {
Base::m_proto->acquire();
Base::GIWrapperBase::debug_lifecycle(obj, "Instance constructor");
}
+
+ explicit GIWrapperInstance(JSContext* cx, JS::HandleObject obj)
+ : GIWrapperInstance(Prototype::for_js_prototype(cx, obj), obj) {}
+
~GIWrapperInstance(void) { Base::m_proto->release(); }
public:
@@ -981,6 +1067,19 @@ class GIWrapperInstance : public Base {
return priv;
}
+ [[nodiscard]] static Instance* new_for_js_object(Prototype* prototype,
+ JS::HandleObject obj) {
+ g_assert(!JS_GetPrivate(obj));
+ auto* priv = new Instance(prototype, obj);
+
+ // Init the private variable before we do anything else. If a garbage
+ // collection happens when calling the constructor, then this object
+ // might be traced and we would end up dereferencing a null pointer.
+ JS_SetPrivate(obj, priv);
+
+ return priv;
+ }
+
// Method to get an existing Instance
/*
diff --git a/gjs/atoms.h b/gjs/atoms.h
index e44d529a..79951c20 100644
--- a/gjs/atoms.h
+++ b/gjs/atoms.h
@@ -75,6 +75,7 @@ class JSTracer;
#define FOR_EACH_SYMBOL_ATOM(macro) \
macro(gobject_type, "__GObject__type") \
+ macro(gobject_prototype, "__GObject__prototype") \
macro(hook_up_vfunc, "__GObject__hook_up_vfunc") \
macro(private_ns_marker, "__gjsPrivateNS") \
macro(signal_find, "__GObject__signal_find") \
diff --git a/installed-tests/js/meson.build b/installed-tests/js/meson.build
index b42f3b20..be17f680 100644
--- a/installed-tests/js/meson.build
+++ b/installed-tests/js/meson.build
@@ -123,6 +123,7 @@ jasmine_tests = [
'GLib',
'GObject',
'GObjectClass',
+ 'GObjectType',
'GObjectInterface',
'GObjectValue',
'GTypeClass',
diff --git a/installed-tests/js/testGObjectType.js b/installed-tests/js/testGObjectType.js
new file mode 100644
index 00000000..b228621a
--- /dev/null
+++ b/installed-tests/js/testGObjectType.js
@@ -0,0 +1,1712 @@
+// -*- mode: js; indent-tabs-mode: nil -*-
+// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
+// SPDX-FileCopyrightText: 2011 Giovanni Campagna <gcampagna src gnome org>
+
+const System = imports.system;
+
+imports.gi.versions.Gtk = '3.0';
+
+const Gio = imports.gi.Gio;
+const GLib = imports.gi.GLib;
+const GObject = imports.gi.GObject;
+const Gtk = imports.gi.Gtk;
+
+const MyRegisteredObject = GObject.registerClass(class MyRegisteredObject extends GObject.Object {
+ static [GObject.properties] = {
+ readwrite: GObject.ParamSpec.string(
+ 'readwrite',
+ 'ParamReadwrite',
+ 'A read write parameter',
+ GObject.ParamFlags.READWRITE,
+ ''
+ ),
+ readonly: GObject.ParamSpec.string(
+ 'readonly',
+ 'ParamReadonly',
+ 'A readonly parameter',
+ GObject.ParamFlags.READABLE,
+ ''
+ ),
+ construct: GObject.ParamSpec.string(
+ 'construct',
+ 'ParamConstructOnly',
+ 'A readwrite construct-only parameter',
+ GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY,
+ 'default'
+ ),
+ };
+
+ static [GObject.signals] = {
+ empty: {},
+ minimal: {param_types: [GObject.TYPE_INT, GObject.TYPE_INT]},
+ full: {
+ flags: GObject.SignalFlags.RUN_LAST,
+ accumulator: GObject.AccumulatorType.FIRST_WINS,
+ return_type: GObject.TYPE_INT,
+ param_types: [],
+ },
+ 'run-last': {flags: GObject.SignalFlags.RUN_LAST},
+ detailed: {
+ flags: GObject.SignalFlags.RUN_FIRST | GObject.SignalFlags.DETAILED,
+ param_types: [GObject.TYPE_STRING],
+ },
+ };
+
+ get readwrite() {
+ if (typeof this._readwrite === 'undefined')
+ return 'foo';
+ return this._readwrite;
+ }
+
+ set readwrite(val) {
+ if (val === 'ignore')
+ return;
+
+ this._readwrite = val;
+ }
+
+ get readonly() {
+ if (typeof this._readonly === 'undefined')
+ return 'bar';
+ return this._readonly;
+ }
+
+ set readonly(val) {
+ // this should never be called
+ void val;
+ this._readonly = 'bogus';
+ }
+
+ get construct() {
+ if (typeof this._constructProp === 'undefined')
+ return null;
+ return this._constructProp;
+ }
+
+ set construct(val) {
+ this._constructProp = val;
+ }
+
+ notifyProp() {
+ this._readonly = 'changed';
+
+ this.notify('readonly');
+ }
+
+ emitEmpty() {
+ this.emit('empty');
+ }
+
+ emitMinimal(one, two) {
+ this.emit('minimal', one, two);
+ }
+
+ emitFull() {
+ return this.emit('full');
+ }
+
+ emitDetailed() {
+ this.emit('detailed::one');
+ this.emit('detailed::two');
+ }
+
+ emitRunLast(callback) {
+ this._run_last_callback = callback;
+ this.emit('run-last');
+ }
+
+ on_run_last() {
+ this._run_last_callback();
+ }
+
+ on_empty() {
+ this.empty_called = true;
+ }
+
+ on_full() {
+ this.full_default_handler_called = true;
+ return 79;
+ }
+});
+
+class MyObject extends GObject.Object {
+ static [GObject.properties] = {
+ readwrite: GObject.ParamSpec.string(
+ 'readwrite',
+ 'ParamReadwrite',
+ 'A read write parameter',
+ GObject.ParamFlags.READWRITE,
+ ''
+ ),
+ readonly: GObject.ParamSpec.string(
+ 'readonly',
+ 'ParamReadonly',
+ 'A readonly parameter',
+ GObject.ParamFlags.READABLE,
+ ''
+ ),
+ construct: GObject.ParamSpec.string(
+ 'construct',
+ 'ParamConstructOnly',
+ 'A readwrite construct-only parameter',
+ GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY,
+ 'default'
+ ),
+ };
+
+ static [GObject.signals] = {
+ empty: {},
+ minimal: {param_types: [GObject.TYPE_INT, GObject.TYPE_INT]},
+ full: {
+ flags: GObject.SignalFlags.RUN_LAST,
+ accumulator: GObject.AccumulatorType.FIRST_WINS,
+ return_type: GObject.TYPE_INT,
+ param_types: [],
+ },
+ 'run-last': {flags: GObject.SignalFlags.RUN_LAST},
+ detailed: {
+ flags: GObject.SignalFlags.RUN_FIRST | GObject.SignalFlags.DETAILED,
+ param_types: [GObject.TYPE_STRING],
+ },
+ };
+
+ get readwrite() {
+ if (typeof this._readwrite === 'undefined')
+ return 'foo';
+ return this._readwrite;
+ }
+
+ set readwrite(val) {
+ if (val === 'ignore')
+ return;
+
+ this._readwrite = val;
+ }
+
+ get readonly() {
+ if (typeof this._readonly === 'undefined')
+ return 'bar';
+ return this._readonly;
+ }
+
+ set readonly(val) {
+ // this should never be called
+ void val;
+ this._readonly = 'bogus';
+ }
+
+ get construct() {
+ if (typeof this._constructProp === 'undefined')
+ return null;
+ return this._constructProp;
+ }
+
+ set construct(val) {
+ this._constructProp = val;
+ }
+
+ notifyProp() {
+ this._readonly = 'changed';
+
+ this.notify('readonly');
+ }
+
+ emitEmpty() {
+ this.emit('empty');
+ }
+
+ emitMinimal(one, two) {
+ this.emit('minimal', one, two);
+ }
+
+ emitFull() {
+ return this.emit('full');
+ }
+
+ emitDetailed() {
+ this.emit('detailed::one');
+ this.emit('detailed::two');
+ }
+
+ emitRunLast(callback) {
+ this._run_last_callback = callback;
+ this.emit('run-last');
+ }
+
+ on_run_last() {
+ this._run_last_callback();
+ }
+
+ on_empty() {
+ this.empty_called = true;
+ }
+
+ on_full() {
+ this.full_default_handler_called = true;
+ return 79;
+ }
+}
+
+GObject.registerType(MyObject);
+
+class MyAbstractObject extends GObject.Object {
+ static [GObject.GTypeFlags] = GObject.TypeFlags.ABSTRACT;
+}
+
+class MyApplication extends Gio.Application {
+ static [GObject.signals] = {
+ custom: {param_types: [GObject.TYPE_INT]},
+ };
+
+ emitCustom(n) {
+ this.emit('custom', n);
+ }
+}
+
+GObject.registerType(MyApplication);
+
+class MyInitable extends GObject.Object {
+ static [GObject.interfaces] = [Gio.Initable];
+
+ vfunc_init(cancellable) {
+ if (!(cancellable instanceof Gio.Cancellable))
+ throw new Error('Bad argument');
+
+ this.inited = true;
+ }
+}
+
+GObject.registerType(MyInitable);
+
+class Derived extends MyObject {
+ constructor() {
+ super({readwrite: 'yes'});
+ }
+}
+
+GObject.registerType(Derived);
+
+class Cla$$ extends MyObject {}
+
+GObject.registerType(Cla$$);
+
+class MyCustomInit extends GObject.Object {
+ _instance_init() {
+ this.foo = true;
+ }
+}
+
+GObject.registerType(MyCustomInit);
+
+const NoName = class extends GObject.Object {};
+
+GObject.registerType(NoName);
+
+describe('GObject class with decorator', function () {
+ let myInstance;
+ beforeEach(function () {
+ myInstance = new MyObject();
+ });
+
+ it('throws an error when not used with a GObject-derived class', function () {
+ class Foo {}
+ class Bar extends Foo {}
+ expect(() => GObject.registerType(Bar)).toThrow();
+ });
+
+ it('throws an error when used with an abstract class', function () {
+ expect(() => new MyAbstractObject()).toThrow();
+ });
+
+ it('constructs with default values for properties', function () {
+ expect(myInstance.readwrite).toEqual('foo');
+ expect(myInstance.readonly).toEqual('bar');
+ expect(myInstance.construct).toEqual('default');
+ });
+
+ it('constructs with a hash of property values', function () {
+ let myInstance2 = new MyObject({readwrite: 'baz', construct: 'asdf'});
+ expect(myInstance2.readwrite).toEqual('baz');
+ expect(myInstance2.readonly).toEqual('bar');
+ expect(myInstance2.construct).toEqual('asdf');
+ });
+
+ it('warns if more than one argument passed to the default constructor', function () {
+ GLib.test_expect_message(
+ 'Gjs',
+ GLib.LogLevelFlags.LEVEL_MESSAGE,
+ '*Too many arguments*'
+ );
+
+ new MyObject({readwrite: 'baz'}, 'this is ignored', 123);
+
+ GLib.test_assert_expected_messages_internal(
+ 'Gjs',
+ 'testGObjectClass.js',
+ 0,
+ 'testGObjectClassTooManyArguments'
+ );
+ });
+
+ it('throws an error if the first argument to the default constructor is not a property hash', function
() {
+ expect(() => new MyObject('this is wrong')).toThrow();
+ });
+
+ it('accepts a property hash that is not a plain object', function () {
+ expect(() => new MyObject(new GObject.Object())).not.toThrow();
+ });
+
+ const ui = `<interface>
+ <object class="Gjs_MyObject" id="MyObject">
+ <property name="readwrite">baz</property>
+ <property name="construct">quz</property>
+ </object>
+ </interface>`;
+
+ it('constructs with property values from Gtk.Builder', function () {
+ let builder = Gtk.Builder.new_from_string(ui, -1);
+ let myInstance3 = builder.get_object('MyObject');
+ expect(myInstance3 instanceof MyObject).toBe(true);
+ expect(myInstance3.readwrite).toEqual('baz');
+ expect(myInstance3.readonly).toEqual('bar');
+ expect(myInstance3.construct).toEqual('quz');
+ });
+
+ it('does not allow changing CONSTRUCT_ONLY properties', function () {
+ myInstance.construct = 'val';
+ expect(myInstance.construct).toEqual('default');
+ });
+
+ it('has a name', function () {
+ expect(MyObject.name).toEqual('MyObject');
+ });
+
+ // the following would (should) cause a CRITICAL:
+ // myInstance.readonly = 'val';
+
+ it('has a notify signal', function () {
+ let notifySpy = jasmine.createSpy('notifySpy');
+ myInstance.connect('notify::readonly', notifySpy);
+
+ myInstance.notifyProp();
+ myInstance.notifyProp();
+
+ expect(notifySpy).toHaveBeenCalledTimes(2);
+ });
+
+ it('can define its own signals', function () {
+ let emptySpy = jasmine.createSpy('emptySpy');
+ myInstance.connect('empty', emptySpy);
+ myInstance.emitEmpty();
+
+ expect(emptySpy).toHaveBeenCalled();
+ expect(myInstance.empty_called).toBeTruthy();
+ });
+
+ it('passes emitted arguments to signal handlers', function () {
+ let minimalSpy = jasmine.createSpy('minimalSpy');
+ myInstance.connect('minimal', minimalSpy);
+ myInstance.emitMinimal(7, 5);
+
+ expect(minimalSpy).toHaveBeenCalledWith(myInstance, 7, 5);
+ });
+
+ it('can return values from signals', function () {
+ let fullSpy = jasmine.createSpy('fullSpy').and.returnValue(42);
+ myInstance.connect('full', fullSpy);
+ let result = myInstance.emitFull();
+
+ expect(fullSpy).toHaveBeenCalled();
+ expect(result).toEqual(42);
+ });
+
+ it('does not call first-wins signal handlers after one returns a value', function () {
+ let neverCalledSpy = jasmine.createSpy('neverCalledSpy');
+ myInstance.connect('full', () => 42);
+ myInstance.connect('full', neverCalledSpy);
+ myInstance.emitFull();
+
+ expect(neverCalledSpy).not.toHaveBeenCalled();
+ expect(myInstance.full_default_handler_called).toBeFalsy();
+ });
+
+ it('gets the return value of the default handler', function () {
+ let result = myInstance.emitFull();
+
+ expect(myInstance.full_default_handler_called).toBeTruthy();
+ expect(result).toEqual(79);
+ });
+
+ it('calls run-last default handler last', function () {
+ let stack = [];
+ let runLastSpy = jasmine.createSpy('runLastSpy').and.callFake(() => {
+ stack.push(1);
+ });
+ myInstance.connect('run-last', runLastSpy);
+ myInstance.emitRunLast(() => {
+ stack.push(2);
+ });
+
+ expect(stack).toEqual([1, 2]);
+ });
+
+ it("can inherit from something that's not GObject.Object", function () {
+ // ...and still get all the goodies of GObject.Class
+ let instance = new MyApplication({
+ application_id: 'org.gjs.Application',
+ });
+ let customSpy = jasmine.createSpy('customSpy');
+ instance.connect('custom', customSpy);
+
+ instance.emitCustom(73);
+ expect(customSpy).toHaveBeenCalledWith(instance, 73);
+ });
+
+ it('can implement an interface', function () {
+ let instance = new MyInitable();
+ expect(instance instanceof Gio.Initable).toBeTruthy();
+ expect(instance instanceof Gio.AsyncInitable).toBeFalsy();
+ });
+
+ it('can implement interface vfuncs', function () {
+ let instance = new MyInitable();
+ expect(instance.inited).toBeFalsy();
+
+ instance.init(new Gio.Cancellable());
+ expect(instance.inited).toBeTruthy();
+ });
+
+ it('can be a subclass', function () {
+ let derived = new Derived();
+
+ expect(derived instanceof Derived).toBeTruthy();
+ expect(derived instanceof MyObject).toBeTruthy();
+
+ expect(derived.readwrite).toEqual('yes');
+ });
+
+ it('can have any valid class name', function () {
+ let obj = new Cla$$();
+
+ expect(obj instanceof Cla$$).toBeTruthy();
+ expect(obj instanceof MyObject).toBeTruthy();
+ });
+
+ it('handles anonymous class expressions', function () {
+ const obj = new NoName();
+ expect(obj instanceof NoName).toBeTruthy();
+
+ const NoName2 = class extends GObject.Object {};
+ GObject.registerType(NoName2);
+ const obj2 = new NoName2();
+ expect(obj2 instanceof NoName2).toBeTruthy();
+ });
+
+ it('calls its _instance_init() function while chaining up in constructor', function () {
+ let instance = new MyCustomInit();
+ expect(instance.foo).toBeTruthy();
+ });
+
+ it('can have an interface-valued property', function () {
+ class InterfacePropObject extends GObject.Object {
+ static [GObject.properties] = {
+ file: GObject.ParamSpec.object(
+ 'file',
+ 'File',
+ 'File',
+ GObject.ParamFlags.READWRITE |
+ GObject.ParamFlags.CONSTRUCT_ONLY,
+ Gio.File.$gtype
+ ),
+ };
+ }
+
+ GObject.registerType(InterfacePropObject);
+ let file = Gio.File.new_for_path('dummy');
+ expect(() => new InterfacePropObject({file})).not.toThrow();
+ });
+
+ it('can override a property from the parent class', function () {
+ class OverrideObject extends MyObject {
+ static [GObject.properties] = {
+ readwrite: GObject.ParamSpec.override('readwrite', MyObject),
+ };
+
+ get readwrite() {
+ return this._subclass_readwrite;
+ }
+
+ set readwrite(val) {
+ this._subclass_readwrite = `subclass${val}`;
+ }
+ }
+ GObject.registerType(OverrideObject);
+ let obj = new OverrideObject();
+ obj.readwrite = 'foo';
+ expect(obj.readwrite).toEqual('subclassfoo');
+ });
+
+ it('cannot override a non-existent property', function () {
+ expect(() => {
+ class BadOverride extends GObject.Object {
+ static [GObject.properties] = {
+ nonexistent: GObject.ParamSpec.override(
+ 'nonexistent',
+ GObject.Object
+ ),
+ };
+ }
+ }).toThrow();
+ });
+
+ it('handles gracefully forgetting to override a C property', function () {
+ GLib.test_expect_message(
+ 'GLib-GObject',
+ GLib.LogLevelFlags.LEVEL_CRITICAL,
+ "*Object class Gjs_ForgottenOverride doesn't implement property " +
+ "'anchors' from interface 'GTlsFileDatabase'*"
+ );
+
+ class ForgottenOverride extends Gio.TlsDatabase {
+ static [GObject.interfaces] = [Gio.TlsFileDatabase];
+ }
+ // This is a random interface in Gio with a read-write property
+ GObject.registerType(ForgottenOverride);
+ const obj = new ForgottenOverride();
+ expect(obj.anchors).not.toBeDefined();
+ expect(() => (obj.anchors = 'foo')).not.toThrow();
+ expect(obj.anchors).toEqual('foo');
+
+ GLib.test_assert_expected_messages_internal(
+ 'Gjs',
+ 'testGObjectClass.js',
+ 0,
+ 'testGObjectClassForgottenOverride'
+ );
+ });
+
+ it('handles gracefully overriding a C property but forgetting the accessors', function () {
+ // This is a random interface in Gio with a read-write property
+ class ForgottenAccessors extends Gio.TlsDatabase {
+ static [GObject.interfaces] = [Gio.TlsFileDatabase];
+
+ static [GObject.properties] = {
+ anchors: GObject.ParamSpec.override(
+ 'anchors',
+ Gio.TlsFileDatabase
+ ),
+ };
+ }
+ GObject.registerType(ForgottenAccessors);
+ const obj = new ForgottenAccessors();
+ expect(obj.anchors).toBeNull(); // the property's default value
+ obj.anchors = 'foo';
+ expect(obj.anchors).toEqual('foo');
+
+ class ForgottenAccessors2 extends ForgottenAccessors {}
+ GObject.registerType(ForgottenAccessors2);
+ const obj2 = new ForgottenAccessors2();
+ expect(obj2.anchors).toBeNull();
+ obj2.anchors = 'foo';
+ expect(obj2.anchors).toEqual('foo');
+ });
+
+ it('does not pollute the wrong prototype with GObject properties', function () {
+ class MyCustomCharset extends Gio.CharsetConverter {
+ constructor() {
+ super();
+ void this.from_charset;
+ }
+ }
+
+ GObject.registerType(MyCustomCharset);
+
+ class MySecondCustomCharset extends GObject.Object {
+ constructor() {
+ super();
+ this.from_charset = 'another value';
+ }
+ }
+
+ GObject.registerType(MySecondCustomCharset);
+
+ expect(
+ () => new MyCustomCharset() && new MySecondCustomCharset()
+ ).not.toThrow();
+ });
+
+ it('resolves properties from interfaces', function () {
+ const mon = Gio.NetworkMonitor.get_default();
+ expect(mon.network_available).toBeDefined();
+ expect(mon.networkAvailable).toBeDefined();
+ expect(mon['network-available']).toBeDefined();
+ });
+
+ it('has a toString() definition', function () {
+ expect(myInstance.toString()).toMatch(
+ /\[object instance wrapper GType:Gjs_MyObject jsobj@0x[a-f0-9]+ native@0x[a-f0-9]+\]/
+ );
+ expect(new Derived().toString()).toMatch(
+ /\[object instance wrapper GType:Gjs_Derived jsobj@0x[a-f0-9]+ native@0x[a-f0-9]+\]/
+ );
+ });
+});
+
+describe('GObject virtual function', function () {
+ it('can have its property read', function () {
+ expect(GObject.Object.prototype.vfunc_constructed).toBeTruthy();
+ });
+
+ it('can have its property overridden with an anonymous function', function () {
+ let callback;
+
+ let key = 'vfunc_constructed';
+
+ class _SimpleTestClass1 extends GObject.Object {
+ static [GObject.GTypeName] = 'SimpleTestClass1';
+ }
+
+ if (GObject.Object.prototype.vfunc_constructed) {
+ let parentFunc = GObject.Object.prototype.vfunc_constructed;
+ _SimpleTestClass1.prototype[key] = function (...args) {
+ parentFunc.call(this, ...args);
+ callback('123');
+ };
+ } else {
+ _SimpleTestClass1.prototype[key] = function () {
+ callback('abc');
+ };
+ }
+
+ callback = jasmine.createSpy('callback');
+
+ GObject.registerType(_SimpleTestClass1);
+ new _SimpleTestClass1();
+
+ expect(callback).toHaveBeenCalledWith('123');
+ });
+
+ it('can access the parent prototype with super()', function () {
+ let callback;
+
+ class _SimpleTestClass2 extends GObject.Object {
+ static [GObject.GTypeName] = 'SimpleTestClass2';
+
+ vfunc_constructed() {
+ super.vfunc_constructed();
+ callback('vfunc_constructed');
+ }
+ }
+
+ callback = jasmine.createSpy('callback');
+
+ GObject.registerType(_SimpleTestClass2);
+ new _SimpleTestClass2();
+
+ expect(callback).toHaveBeenCalledWith('vfunc_constructed');
+ });
+
+ it('handles non-existing properties', function () {
+ class _SimpleTestClass3 extends GObject.Object {
+ static [GObject.GTypeName] = 'SimpleTestClass3';
+ }
+
+ _SimpleTestClass3.prototype.vfunc_doesnt_exist = function () {};
+
+ if (GObject.Object.prototype.vfunc_doesnt_exist)
+ fail('Virtual function should not exist');
+
+ expect(() => GObject.registerType(_SimpleTestClass3)).toThrow();
+ });
+
+ it('gracefully bails out when overriding an unsupported vfunc type', function () {
+ class Foo extends GObject.Object {
+ static [GObject.interfaces] = [Gio.AsyncInitable];
+
+ vfunc_init_async() {}
+ }
+
+ expect(() => GObject.registerType(Foo)).toThrow();
+ });
+});
+
+describe('GObject creation using base classes without registered GType', function () {
+ it('fails when trying to instantiate a class that inherits from a GObject type', function () {
+ const BadInheritance = class extends GObject.Object {};
+ const BadDerivedInheritance = class extends Derived {};
+
+ expect(() => new BadInheritance()).toThrowError(
+ /Tried to construct an object without a GType/
+ );
+ expect(() => new BadDerivedInheritance()).toThrowError(
+ /Tried to construct an object without a GType/
+ );
+ });
+
+ it('fails when trying to register a GObject class that inherits from a non-GObject type', function () {
+ class BadInheritance extends GObject.Object {}
+ class BadInheritanceDerived extends BadInheritance {}
+ expect(() => GObject.registerType(BadInheritanceDerived)).toThrowError(
+ /Object 0x[a-f0-9]+ is not a subclass of GObject_Object, it's a Object/
+ );
+ });
+});
+
+describe('Register GType name', function () {
+ beforeAll(function () {
+ expect(GObject.gtypeNameBasedOnJSPath).toBeFalsy();
+ });
+
+ afterEach(function () {
+ GObject.gtypeNameBasedOnJSPath = false;
+ });
+
+ it('uses the class name', function () {
+ class GTypeTestAutoName extends GObject.Object {}
+ GObject.registerType(GTypeTestAutoName);
+
+ expect(GTypeTestAutoName.$gtype.name).toEqual('Gjs_GTypeTestAutoName');
+ });
+
+ it('uses the sanitized class name', function () {
+ class GTypeTestAutoCla$$Name extends GObject.Object {}
+ GObject.registerType(GTypeTestAutoCla$$Name);
+
+ expect(GTypeTestAutoCla$$Name.$gtype.name).toEqual(
+ 'Gjs_GTypeTestAutoCla__Name'
+ );
+ });
+
+ it('use the file path and class name', function () {
+ GObject.gtypeNameBasedOnJSPath = true;
+ class GTypeTestAutoName extends GObject.Object {}
+ GObject.registerType(GTypeTestAutoName);
+
+ /* Update this test if the file is moved */
+ expect(GTypeTestAutoName.$gtype.name).toEqual(
+ 'Gjs_js_testGObjectType_GTypeTestAutoName'
+ );
+ });
+
+ it('use the file path and sanitized class name', function () {
+ GObject.gtypeNameBasedOnJSPath = true;
+ class GTypeTestAutoCla$$Name extends GObject.Object {}
+ GObject.registerType(GTypeTestAutoCla$$Name);
+
+ /* Update this test if the file is moved */
+ expect(GTypeTestAutoCla$$Name.$gtype.name).toEqual(
+ 'Gjs_js_testGObjectType_GTypeTestAutoCla__Name'
+ );
+ });
+
+ it('use provided class name', function () {
+ class GtypeClass extends GObject.Object {
+ static [GObject.GTypeName] = 'GTypeTestManualName';
+ }
+ GObject.registerType(GtypeClass);
+
+ expect(GtypeClass.$gtype.name).toEqual('GTypeTestManualName');
+ });
+
+ it('sanitizes user provided class name', function () {
+ let gtypeName = "GType$Test/WithLòt's of*bad§chars!";
+ let expectedSanitized = 'GType_Test_WithL_t_s_of_bad_chars_';
+
+ GLib.test_expect_message(
+ 'Gjs',
+ GLib.LogLevelFlags.LEVEL_WARNING,
+ `*RangeError: Provided GType name '${gtypeName}' is not valid; ` +
+ `automatically sanitized to '${expectedSanitized}'*`
+ );
+
+ class GtypeClass extends GObject.Object {
+ static [GObject.GTypeName] = gtypeName;
+ }
+ GObject.registerType(GtypeClass);
+
+ GLib.test_assert_expected_messages_internal(
+ 'Gjs',
+ 'testGObjectClass.js',
+ 0,
+ 'testGObjectRegisterClassSanitize'
+ );
+
+ expect(GtypeClass.$gtype.name).toEqual(expectedSanitized);
+ });
+});
+
+describe('Signal handler matching', function () {
+ let o,
+ handleEmpty,
+ emptyId,
+ handleDetailed,
+ detailedId,
+ handleDetailedOne,
+ detailedOneId,
+ handleDetailedTwo,
+ detailedTwoId,
+ handleNotifyTwo,
+ notifyTwoId,
+ handleMinimalOrFull,
+ minimalId,
+ fullId;
+
+ beforeEach(function () {
+ o = new MyObject();
+ handleEmpty = jasmine.createSpy('handleEmpty');
+ emptyId = o.connect('empty', handleEmpty);
+ handleDetailed = jasmine.createSpy('handleDetailed');
+ detailedId = o.connect('detailed', handleDetailed);
+ handleDetailedOne = jasmine.createSpy('handleDetailedOne');
+ detailedOneId = o.connect('detailed::one', handleDetailedOne);
+ handleDetailedTwo = jasmine.createSpy('handleDetailedTwo');
+ detailedTwoId = o.connect('detailed::two', handleDetailedTwo);
+ handleNotifyTwo = jasmine.createSpy('handleNotifyTwo');
+ notifyTwoId = o.connect('notify::two', handleNotifyTwo);
+ handleMinimalOrFull = jasmine.createSpy('handleMinimalOrFull');
+ minimalId = o.connect('minimal', handleMinimalOrFull);
+ fullId = o.connect('full', handleMinimalOrFull);
+ });
+
+ it('finds handlers by signal ID', function () {
+ expect(GObject.signal_handler_find(o, {signalId: 'empty'})).toEqual(
+ emptyId
+ );
+ // when more than one are connected, returns an arbitrary one
+ expect([detailedId, detailedOneId, detailedTwoId]).toContain(
+ GObject.signal_handler_find(o, {signalId: 'detailed'})
+ );
+ });
+
+ it('finds handlers by signal detail', function () {
+ expect(GObject.signal_handler_find(o, {detail: 'one'})).toEqual(
+ detailedOneId
+ );
+ // when more than one are connected, returns an arbitrary one
+ expect([detailedTwoId, notifyTwoId]).toContain(
+ GObject.signal_handler_find(o, {detail: 'two'})
+ );
+ });
+
+ it('finds handlers by callback', function () {
+ expect(GObject.signal_handler_find(o, {func: handleEmpty})).toEqual(
+ emptyId
+ );
+ expect(
+ GObject.signal_handler_find(o, {func: handleDetailed})
+ ).toEqual(detailedId);
+ expect(
+ GObject.signal_handler_find(o, {func: handleDetailedOne})
+ ).toEqual(detailedOneId);
+ expect(
+ GObject.signal_handler_find(o, {func: handleDetailedTwo})
+ ).toEqual(detailedTwoId);
+ expect(
+ GObject.signal_handler_find(o, {func: handleNotifyTwo})
+ ).toEqual(notifyTwoId);
+ // when more than one are connected, returns an arbitrary one
+ expect([minimalId, fullId]).toContain(
+ GObject.signal_handler_find(o, {func: handleMinimalOrFull})
+ );
+ });
+
+ it('finds handlers by a combination of parameters', function () {
+ expect(
+ GObject.signal_handler_find(o, {
+ signalId: 'detailed',
+ detail: 'two',
+ })
+ ).toEqual(detailedTwoId);
+ expect(
+ GObject.signal_handler_find(o, {
+ signalId: 'detailed',
+ func: handleDetailed,
+ })
+ ).toEqual(detailedId);
+ });
+
+ it('blocks a handler by callback', function () {
+ expect(
+ GObject.signal_handlers_block_matched(o, {func: handleEmpty})
+ ).toEqual(1);
+ o.emitEmpty();
+ expect(handleEmpty).not.toHaveBeenCalled();
+
+ expect(
+ GObject.signal_handlers_unblock_matched(o, {func: handleEmpty})
+ ).toEqual(1);
+ o.emitEmpty();
+ expect(handleEmpty).toHaveBeenCalled();
+ });
+
+ it('blocks multiple handlers by callback', function () {
+ expect(
+ GObject.signal_handlers_block_matched(o, {
+ func: handleMinimalOrFull,
+ })
+ ).toEqual(2);
+ o.emitMinimal();
+ o.emitFull();
+ expect(handleMinimalOrFull).not.toHaveBeenCalled();
+
+ expect(
+ GObject.signal_handlers_unblock_matched(o, {
+ func: handleMinimalOrFull,
+ })
+ ).toEqual(2);
+ o.emitMinimal();
+ o.emitFull();
+ expect(handleMinimalOrFull).toHaveBeenCalledTimes(2);
+ });
+
+ it('blocks handlers by a combination of parameters', function () {
+ expect(
+ GObject.signal_handlers_block_matched(o, {
+ signalId: 'detailed',
+ func: handleDetailed,
+ })
+ ).toEqual(1);
+ o.emit('detailed', '');
+ o.emit('detailed::one', '');
+ expect(handleDetailed).not.toHaveBeenCalled();
+ expect(handleDetailedOne).toHaveBeenCalled();
+
+ expect(
+ GObject.signal_handlers_unblock_matched(o, {
+ signalId: 'detailed',
+ func: handleDetailed,
+ })
+ ).toEqual(1);
+ o.emit('detailed', '');
+ o.emit('detailed::one', '');
+ expect(handleDetailed).toHaveBeenCalled();
+ });
+
+ it('disconnects a handler by callback', function () {
+ expect(
+ GObject.signal_handlers_disconnect_matched(o, {func: handleEmpty})
+ ).toEqual(1);
+ o.emitEmpty();
+ expect(handleEmpty).not.toHaveBeenCalled();
+ });
+
+ it('blocks multiple handlers by callback', function () {
+ expect(
+ GObject.signal_handlers_disconnect_matched(o, {
+ func: handleMinimalOrFull,
+ })
+ ).toEqual(2);
+ o.emitMinimal();
+ o.emitFull();
+ expect(handleMinimalOrFull).not.toHaveBeenCalled();
+ });
+
+ it('blocks handlers by a combination of parameters', function () {
+ expect(
+ GObject.signal_handlers_disconnect_matched(o, {
+ signalId: 'detailed',
+ func: handleDetailed,
+ })
+ ).toEqual(1);
+ o.emit('detailed', '');
+ o.emit('detailed::one', '');
+ expect(handleDetailed).not.toHaveBeenCalled();
+ expect(handleDetailedOne).toHaveBeenCalled();
+ });
+
+ it('blocks a handler by callback, convenience method', function () {
+ expect(GObject.signal_handlers_block_by_func(o, handleEmpty)).toEqual(
+ 1
+ );
+ o.emitEmpty();
+ expect(handleEmpty).not.toHaveBeenCalled();
+
+ expect(GObject.signal_handlers_unblock_by_func(o, handleEmpty)).toEqual(
+ 1
+ );
+ o.emitEmpty();
+ expect(handleEmpty).toHaveBeenCalled();
+ });
+
+ it('disconnects a handler by callback, convenience method', function () {
+ expect(
+ GObject.signal_handlers_disconnect_by_func(o, handleEmpty)
+ ).toEqual(1);
+ o.emitEmpty();
+ expect(handleEmpty).not.toHaveBeenCalled();
+ });
+
+ it('does not support disconnecting a handler by callback data', function () {
+ expect(() =>
+ GObject.signal_handlers_disconnect_by_data(o, null)
+ ).toThrow();
+ });
+});
+
+describe('Auto accessor generation', function () {
+ class AutoAccessors extends GObject.Object {
+ constructor(props = {}) {
+ super(props);
+ this._snakeNameGetterCalled = 0;
+ this._snakeNameSetterCalled = 0;
+ this._camelNameGetterCalled = 0;
+ this._camelNameSetterCalled = 0;
+ this._kebabNameGetterCalled = 0;
+ this._kebabNameSetterCalled = 0;
+ }
+
+ static [GObject.properties] = {
+ simple: GObject.ParamSpec.int(
+ 'simple',
+ 'Simple',
+ 'Short-named property',
+ GObject.ParamFlags.READWRITE,
+ 0,
+ 100,
+ 24
+ ),
+ 'long-long-name': GObject.ParamSpec.int(
+ 'long-long-name',
+ 'Long long name',
+ 'Long-named property',
+ GObject.ParamFlags.READWRITE,
+ 0,
+ 100,
+ 48
+ ),
+ construct: GObject.ParamSpec.int(
+ 'construct',
+ 'Construct',
+ 'Construct',
+ GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT,
+ 0,
+ 100,
+ 96
+ ),
+ 'construct-only': GObject.ParamSpec.int(
+ 'construct-only',
+ 'Construct only',
+ 'Construct-only property',
+ GObject.ParamFlags.READWRITE |
+ GObject.ParamFlags.CONSTRUCT_ONLY,
+ 0,
+ 100,
+ 80
+ ),
+ 'snake-name': GObject.ParamSpec.int(
+ 'snake-name',
+ 'Snake name',
+ 'Snake-cased property',
+ GObject.ParamFlags.READWRITE,
+ 0,
+ 100,
+ 36
+ ),
+ 'camel-name': GObject.ParamSpec.int(
+ 'camel-name',
+ 'Camel name',
+ 'Camel-cased property',
+ GObject.ParamFlags.READWRITE,
+ 0,
+ 100,
+ 72
+ ),
+ 'kebab-name': GObject.ParamSpec.int(
+ 'kebab-name',
+ 'Kebab name',
+ 'Kebab-cased property',
+ GObject.ParamFlags.READWRITE,
+ 0,
+ 100,
+ 12
+ ),
+ readonly: GObject.ParamSpec.int(
+ 'readonly',
+ 'Readonly',
+ 'Readonly property',
+ GObject.ParamFlags.READABLE,
+ 0,
+ 100,
+ 54
+ ),
+ writeonly: GObject.ParamSpec.int(
+ 'writeonly',
+ 'Writeonly',
+ 'Writeonly property',
+ GObject.ParamFlags.WRITABLE,
+ 0,
+ 100,
+ 60
+ ),
+ 'missing-getter': GObject.ParamSpec.int(
+ 'missing-getter',
+ 'Missing getter',
+ 'Missing a getter',
+ GObject.ParamFlags.READWRITE,
+ 0,
+ 100,
+ 18
+ ),
+ 'missing-setter': GObject.ParamSpec.int(
+ 'missing-setter',
+ 'Missing setter',
+ 'Missing a setter',
+ GObject.ParamFlags.READWRITE,
+ 0,
+ 100,
+ 42
+ ),
+ };
+
+ get snake_name() {
+ this._snakeNameGetterCalled++;
+ return 42;
+ }
+
+ set snake_name(value) {
+ this._snakeNameSetterCalled++;
+ }
+
+ get camelName() {
+ this._camelNameGetterCalled++;
+ return 42;
+ }
+
+ set camelName(value) {
+ this._camelNameSetterCalled++;
+ }
+
+ get ['kebab-name']() {
+ this._kebabNameGetterCalled++;
+ return 42;
+ }
+
+ set ['kebab-name'](value) {
+ this._kebabNameSetterCalled++;
+ }
+
+ set missing_getter(value) {
+ this._missingGetter = value;
+ }
+
+ get missing_setter() {
+ return 42;
+ }
+ }
+
+ GObject.registerType(AutoAccessors);
+
+ let a;
+ beforeEach(function () {
+ a = new AutoAccessors();
+ });
+
+ it('get and set the property', function () {
+ a.simple = 1;
+ expect(a.simple).toEqual(1);
+ a['long-long-name'] = 1;
+ expect(a['long-long-name']).toEqual(1);
+ a.construct = 1;
+ expect(a.construct).toEqual(1);
+ });
+
+ it("initial value is the param spec's default value", function () {
+ expect(a.simple).toEqual(24);
+ expect(a.long_long_name).toEqual(48);
+ expect(a.longLongName).toEqual(48);
+ expect(a['long-long-name']).toEqual(48);
+ expect(a.construct).toEqual(96);
+ expect(a.construct_only).toEqual(80);
+ expect(a.constructOnly).toEqual(80);
+ expect(a['construct-only']).toEqual(80);
+ });
+
+ it('set properties at construct time', function () {
+ a = new AutoAccessors({
+ simple: 1,
+ longLongName: 1,
+ construct: 1,
+ 'construct-only': 1,
+ });
+ expect(a.simple).toEqual(1);
+ expect(a.long_long_name).toEqual(1);
+ expect(a.longLongName).toEqual(1);
+ expect(a['long-long-name']).toEqual(1);
+ expect(a.construct).toEqual(1);
+ expect(a.construct_only).toEqual(1);
+ expect(a.constructOnly).toEqual(1);
+ expect(a['construct-only']).toEqual(1);
+ });
+
+ it('notify when the property changes', function () {
+ const notify = jasmine.createSpy('notify');
+ a.connect('notify::simple', notify);
+ a.simple = 1;
+ expect(notify).toHaveBeenCalledTimes(1);
+ notify.calls.reset();
+ a.simple = 1;
+ expect(notify).not.toHaveBeenCalled();
+ });
+
+ it('copies accessors for camel and kebab if snake accessors given', function () {
+ a.snakeName = 42;
+ expect(a.snakeName).toEqual(42);
+ a['snake-name'] = 42;
+ expect(a['snake-name']).toEqual(42);
+ expect(a._snakeNameGetterCalled).toEqual(2);
+ expect(a._snakeNameSetterCalled).toEqual(2);
+ });
+
+ it('copies accessors for snake and kebab if camel accessors given', function () {
+ a.camel_name = 42;
+ expect(a.camel_name).toEqual(42);
+ a['camel-name'] = 42;
+ expect(a['camel-name']).toEqual(42);
+ expect(a._camelNameGetterCalled).toEqual(2);
+ expect(a._camelNameSetterCalled).toEqual(2);
+ });
+
+ it('copies accessors for snake and camel if kebab accessors given', function () {
+ a.kebabName = 42;
+ expect(a.kebabName).toEqual(42);
+ a.kebab_name = 42;
+ expect(a.kebab_name).toEqual(42);
+ expect(a._kebabNameGetterCalled).toEqual(2);
+ expect(a._kebabNameSetterCalled).toEqual(2);
+ });
+
+ it('readonly getter throws', function () {
+ expect(() => a.readonly).toThrowError(/getter/);
+ });
+
+ it('writeonly setter throws', function () {
+ expect(() => (a.writeonly = 1)).toThrowError(/setter/);
+ });
+
+ it('getter throws when setter defined', function () {
+ expect(() => a.missingGetter).toThrowError(/getter/);
+ });
+
+ it('setter throws when getter defined', function () {
+ expect(() => (a.missingSetter = 1)).toThrowError(/setter/);
+ });
+});
+
+class MyObjectWithJSObjectProperty extends GObject.Object {
+ static [GObject.properties] = {
+ 'jsobj-prop': GObject.ParamSpec.jsobject(
+ 'jsobj-prop',
+ 'jsobj-prop',
+ 'jsobj-prop',
+ GObject.ParamFlags.CONSTRUCT | GObject.ParamFlags.READWRITE,
+ ''
+ ),
+ };
+}
+
+GObject.registerType(MyObjectWithJSObjectProperty);
+
+describe('GObject class with JSObject property', function () {
+ it('assigns a valid JSObject on construct', function () {
+ let date = new Date();
+ let obj = new MyObjectWithJSObjectProperty({jsobj_prop: date});
+ expect(obj.jsobj_prop).toEqual(date);
+ expect(obj.jsobj_prop).not.toEqual(new Date(0));
+ expect(() => obj.jsobj_prop.setFullYear(1985)).not.toThrow();
+ expect(obj.jsobj_prop.getFullYear()).toEqual(1985);
+ });
+
+ it('Set null with an empty JSObject on construct', function () {
+ expect(new MyObjectWithJSObjectProperty().jsobj_prop).toBeNull();
+ expect(new MyObjectWithJSObjectProperty({}).jsobj_prop).toBeNull();
+ });
+
+ it('assigns a null JSObject on construct', function () {
+ expect(
+ new MyObjectWithJSObjectProperty({jsobj_prop: null}).jsobj_prop
+ ).toBeNull();
+ });
+
+ it('assigns a JSObject Array on construct', function () {
+ expect(
+ () => new MyObjectWithJSObjectProperty({jsobj_prop: [1, 2, 3]})
+ ).not.toThrow();
+ });
+
+ it('assigns a Function on construct', function () {
+ expect(
+ () =>
+ new MyObjectWithJSObjectProperty({
+ jsobj_prop: () => {
+ return true;
+ },
+ })
+ ).not.toThrow();
+ });
+
+ it('throws an error when using a boolean value on construct', function () {
+ expect(
+ () => new MyObjectWithJSObjectProperty({jsobj_prop: true})
+ ).toThrowError(/JSObject expected/);
+ });
+
+ it('throws an error when using an int value on construct', function () {
+ expect(
+ () => new MyObjectWithJSObjectProperty({jsobj_prop: 1})
+ ).toThrowError(/JSObject expected/);
+ });
+
+ it('throws an error when using a numeric value on construct', function () {
+ expect(
+ () => new MyObjectWithJSObjectProperty({jsobj_prop: Math.PI})
+ ).toThrowError(/JSObject expected/);
+ });
+
+ it('throws an error when using a string value on construct', function () {
+ expect(
+ () => new MyObjectWithJSObjectProperty({jsobj_prop: 'string'})
+ ).toThrowError(/JSObject expected/);
+ });
+
+ it('throws an error when using an undefined value on construct', function () {
+ expect(
+ () => new MyObjectWithJSObjectProperty({jsobj_prop: undefined})
+ ).toThrow();
+ });
+
+ it('property value survives when GObject wrapper is collected', function () {
+ class MyConverter extends GObject.Object {
+ static [GObject.properties] = {
+ testprop: GObject.ParamSpec.jsobject(
+ 'testprop',
+ 'testprop',
+ 'Test property',
+ GObject.ParamFlags.CONSTRUCT | GObject.ParamFlags.READWRITE
+ ),
+ };
+
+ static [GObject.interfaces] = [Gio.Converter];
+ }
+
+ GObject.registerType(MyConverter);
+
+ function stashObject() {
+ const base = new Gio.MemoryInputStream();
+ const converter = new MyConverter({testprop: [1, 2, 3]});
+ return Gio.ConverterInputStream.new(base, converter);
+ }
+
+ const stream = stashObject();
+ System.gc();
+ expect(stream.get_converter().testprop).toEqual([1, 2, 3]);
+ });
+});
+
+class MyObjectWithJSObjectSignals extends GObject.Object {
+ static [GObject.signals] = {
+ 'send-object': {
+ param_types: [GObject.TYPE_JSOBJECT],
+ },
+ 'send-many-objects': {
+ param_types: [
+ GObject.TYPE_JSOBJECT,
+ GObject.TYPE_JSOBJECT,
+ GObject.TYPE_JSOBJECT,
+ ],
+ },
+ 'get-object': {
+ flags: GObject.SignalFlags.RUN_LAST,
+ accumulator: GObject.AccumulatorType.FIRST_WINS,
+ return_type: GObject.TYPE_JSOBJECT,
+ param_types: [GObject.TYPE_JSOBJECT],
+ },
+ };
+
+ emitObject(obj) {
+ this.emit('send-object', obj);
+ }
+}
+
+GObject.registerType(MyObjectWithJSObjectSignals);
+
+describe('GObject class with JSObject signals', function () {
+ let myInstance;
+ beforeEach(function () {
+ myInstance = new MyObjectWithJSObjectSignals();
+ });
+
+ it('emits signal with null JSObject parameter', function () {
+ let customSpy = jasmine.createSpy('sendObjectSpy');
+ myInstance.connect('send-object', customSpy);
+ myInstance.emitObject(null);
+ expect(customSpy).toHaveBeenCalledWith(myInstance, null);
+ });
+
+ it('emits signal with JSObject parameter', function () {
+ let customSpy = jasmine.createSpy('sendObjectSpy');
+ myInstance.connect('send-object', customSpy);
+
+ let obj = {
+ foo: [1, 2, 3],
+ sub: {a: {}, b: this},
+ desc: 'test',
+ date: new Date(),
+ };
+ myInstance.emitObject(obj);
+ expect(customSpy).toHaveBeenCalledWith(myInstance, obj);
+ });
+
+ it('emits signal with multiple JSObject parameters', function () {
+ let customSpy = jasmine.createSpy('sendManyObjectsSpy');
+ myInstance.connect('send-many-objects', customSpy);
+
+ let obj = {
+ foo: [9, 8, 7, 'a', 'b', 'c'],
+ sub: {a: {}, b: this},
+ desc: 'test',
+ date: new RegExp('\\w+'),
+ };
+ myInstance.emit('send-many-objects', obj, obj.foo, obj.sub);
+ expect(customSpy).toHaveBeenCalledWith(
+ myInstance,
+ obj,
+ obj.foo,
+ obj.sub
+ );
+ });
+
+ it('re-emits signal with same JSObject parameter', function () {
+ let obj = {
+ foo: [9, 8, 7, 'a', 'b', 'c'],
+ sub: {a: {}, b: this},
+ func: arg => {
+ return {ret: [arg]};
+ },
+ };
+
+ myInstance.connect('send-many-objects', (instance, func, args, foo) => {
+ expect(instance).toEqual(myInstance);
+ expect(System.addressOf(instance)).toEqual(
+ System.addressOf(myInstance)
+ );
+ expect(foo).toEqual(obj.foo);
+ expect(System.addressOf(foo)).toEqual(System.addressOf(obj.foo));
+ expect(func(args).ret[0]).toEqual(args);
+ });
+ myInstance.connect('send-object', (instance, param) => {
+ expect(instance).toEqual(myInstance);
+ expect(System.addressOf(instance)).toEqual(
+ System.addressOf(myInstance)
+ );
+ expect(param).toEqual(obj);
+ expect(System.addressOf(param)).toEqual(System.addressOf(obj));
+ expect(() =>
+ instance.emit('send-many-objects', param.func, param, param.foo)
+ ).not.toThrow();
+ });
+
+ myInstance.emit('send-object', obj);
+ });
+
+ it('throws an error when using a boolean value as parameter', function () {
+ expect(() => myInstance.emit('send-object', true)).toThrowError(
+ /JSObject expected/
+ );
+ expect(() =>
+ myInstance.emit('send-many-objects', ['a'], true, {})
+ ).toThrowError(/JSObject expected/);
+ });
+
+ it('throws an error when using an int value as parameter', function () {
+ expect(() => myInstance.emit('send-object', 1)).toThrowError(
+ /JSObject expected/
+ );
+ expect(() =>
+ myInstance.emit('send-many-objects', ['a'], 1, {})
+ ).toThrowError(/JSObject expected/);
+ });
+
+ it('throws an error when using a numeric value as parameter', function () {
+ expect(() => myInstance.emit('send-object', Math.PI)).toThrowError(
+ /JSObject expected/
+ );
+ expect(() =>
+ myInstance.emit('send-many-objects', ['a'], Math.PI, {})
+ ).toThrowError(/JSObject expected/);
+ });
+
+ it('throws an error when using a string value as parameter', function () {
+ expect(() => myInstance.emit('send-object', 'string')).toThrowError(
+ /JSObject expected/
+ );
+ expect(() =>
+ myInstance.emit('send-many-objects', ['a'], 'string', {})
+ ).toThrowError(/JSObject expected/);
+ });
+
+ it('throws an error when using an undefined value as parameter', function () {
+ expect(() => myInstance.emit('send-object', undefined)).toThrowError(
+ /JSObject expected/
+ );
+ expect(() =>
+ myInstance.emit('send-many-objects', ['a'], undefined, {})
+ ).toThrowError(/JSObject expected/);
+ });
+
+ it('returns a JSObject', function () {
+ let data = {
+ foo: [9, 8, 7, 'a', 'b', 'c'],
+ sub: {a: {}, b: this},
+ func: arg => {
+ return {ret: [arg]};
+ },
+ };
+ let id = myInstance.connect('get-object', () => {
+ return data;
+ });
+ expect(myInstance.emit('get-object', {})).toBe(data);
+ myInstance.disconnect(id);
+
+ myInstance.connect('get-object', (instance, input) => {
+ if (input) {
+ if (typeof input === 'function')
+ input();
+ return input;
+ }
+
+ class SubObject {
+ constructor() {
+ this.pi = Math.PI;
+ }
+
+ method() {}
+
+ gobject() {
+ return GObject.Object;
+ }
+
+ get data() {
+ return data;
+ }
+ }
+
+ return new SubObject();
+ });
+
+ expect(myInstance.emit('get-object', null).constructor.name).toBe(
+ 'SubObject'
+ );
+ expect(myInstance.emit('get-object', null).data).toBe(data);
+ expect(myInstance.emit('get-object', null).pi).toBe(Math.PI);
+ expect(() =>
+ myInstance.emit('get-object', null).method()
+ ).not.toThrow();
+ expect(myInstance.emit('get-object', null).gobject()).toBe(
+ GObject.Object
+ );
+ expect(
+ new (myInstance.emit('get-object', null).gobject())() instanceof
+ GObject.Object
+ ).toBeTruthy();
+ expect(myInstance.emit('get-object', data)).toBe(data);
+ expect(
+ myInstance.emit('get-object', jasmine.createSpy('callMeSpy'))
+ ).toHaveBeenCalled();
+ });
+
+ it('returns null when returning undefined', function () {
+ myInstance.connect('get-object', () => {
+ return undefined;
+ });
+ expect(myInstance.emit('get-object', {})).toBeNull();
+ });
+
+ it('returns null when not returning', function () {
+ myInstance.connect('get-object', () => {});
+ expect(myInstance.emit('get-object', {})).toBeNull();
+ });
+
+ // These tests are intended to throw an error, but currently errors cannot
+ // be caught from signal handlers, so we check for logged messages instead
+
+ it('throws an error when returning a boolean value', function () {
+ GLib.test_expect_message(
+ 'Gjs',
+ GLib.LogLevelFlags.LEVEL_WARNING,
+ '*JSObject expected*'
+ );
+ myInstance.connect('get-object', () => true);
+ myInstance.emit('get-object', {});
+ GLib.test_assert_expected_messages_internal(
+ 'Gjs',
+ 'testGObjectClass.js',
+ 0,
+ 'throws an error when returning a boolean value'
+ );
+ });
+
+ it('throws an error when returning an int value', function () {
+ GLib.test_expect_message(
+ 'Gjs',
+ GLib.LogLevelFlags.LEVEL_WARNING,
+ '*JSObject expected*'
+ );
+ myInstance.connect('get-object', () => 1);
+ myInstance.emit('get-object', {});
+ GLib.test_assert_expected_messages_internal(
+ 'Gjs',
+ 'testGObjectClass.js',
+ 0,
+ 'throws an error when returning a boolean value'
+ );
+ });
+
+ it('throws an error when returning a numeric value', function () {
+ GLib.test_expect_message(
+ 'Gjs',
+ GLib.LogLevelFlags.LEVEL_WARNING,
+ '*JSObject expected*'
+ );
+ myInstance.connect('get-object', () => Math.PI);
+ myInstance.emit('get-object', {});
+ GLib.test_assert_expected_messages_internal(
+ 'Gjs',
+ 'testGObjectClass.js',
+ 0,
+ 'throws an error when returning a boolean value'
+ );
+ });
+
+ it('throws an error when returning a string value', function () {
+ GLib.test_expect_message(
+ 'Gjs',
+ GLib.LogLevelFlags.LEVEL_WARNING,
+ '*JSObject expected*'
+ );
+ myInstance.connect('get-object', () => 'string');
+ myInstance.emit('get-object', {});
+ GLib.test_assert_expected_messages_internal(
+ 'Gjs',
+ 'testGObjectClass.js',
+ 0,
+ 'throws an error when returning a boolean value'
+ );
+ });
+});
+
+describe('GObject class registered with registerType', function () {
+ class SubObject extends MyRegisteredObject {
+ }
+
+ GObject.registerType(SubObject);
+
+ it('extends class registered with registerClass', function () {
+ expect(() => new SubObject()).not.toThrow();
+
+ const instance = new SubObject();
+
+ expect(instance instanceof SubObject).toBeTrue();
+ expect(instance instanceof GObject.Object).toBeTrue();
+ expect(instance instanceof MyRegisteredObject).toBeTrue();
+ });
+});
diff --git a/modules/core/overrides/GObject.js b/modules/core/overrides/GObject.js
index 6bfaf144..99cb0a07 100644
--- a/modules/core/overrides/GObject.js
+++ b/modules/core/overrides/GObject.js
@@ -23,6 +23,14 @@ var _gtkCssName = Symbol('GTK widget CSS name');
var _gtkInternalChildren = Symbol('GTK widget template internal children');
var _gtkTemplate = Symbol('GTK widget template');
+function assertDerivesFromGObject(klass, functionName) {
+ if (!(klass.prototype instanceof GObject.Object) &&
+ !(klass.prototype instanceof GObject.Interface)) {
+ throw new TypeError(`GObject.${functionName}() used with invalid base ` +
+ `class (is ${Object.getPrototypeOf(klass).name})`);
+ }
+}
+
function registerClass(...args) {
let klass = args[0];
if (args.length === 2) {
@@ -64,11 +72,7 @@ function registerClass(...args) {
klass[_gtkInternalChildren] = metaInfo.InternalChildren;
}
- if (!(klass.prototype instanceof GObject.Object) &&
- !(klass.prototype instanceof GObject.Interface)) {
- throw new TypeError('GObject.registerClass() used with invalid base ' +
- `class (is ${Object.getPrototypeOf(klass).name})`);
- }
+ assertDerivesFromGObject(klass, 'registerClass');
// Find the "least derived" class with a _classInit static function; there
// definitely is one, since this class must inherit from GObject
@@ -78,6 +82,188 @@ function registerClass(...args) {
return initclass._classInit(klass);
}
+function _hookupVFuncs(prototype, gobject_prototype, gtype) {
+ Object.getOwnPropertyNames(prototype)
+ .filter(name => name.startsWith('vfunc_') || name.startsWith('on_'))
+ .forEach(name => {
+ let descr = Object.getOwnPropertyDescriptor(prototype, name);
+ if (typeof descr.value !== 'function')
+ return;
+
+ let func = prototype[name];
+
+ if (name.startsWith('vfunc_')) {
+ gobject_prototype[Gi.hook_up_vfunc_symbol](name.slice(6), func);
+ } else if (name.startsWith('on_')) {
+ let id = GObject.signal_lookup(name.slice(3).replace('_', '-'),
+ gtype);
+ if (id !== 0) {
+ GObject.signal_override_class_closure(id, gtype, function (...argArray) {
+ let emitter = argArray.shift();
+
+ return func.apply(emitter, argArray);
+ });
+ }
+ }
+ });
+}
+
+function _getTypeDefinitions(klass) {
+ let gtypename = _createGTypeName(klass);
+ let gflags = klass.hasOwnProperty(GTypeFlags) ? klass[GTypeFlags] : 0;
+ let gobjectInterfaces = klass.hasOwnProperty(interfaces) ? klass[interfaces] : [];
+ let propertiesArray = _propertiesAsArray(klass);
+ let parent = Object.getPrototypeOf(klass);
+ let gobjectSignals = klass.hasOwnProperty(signals) ? klass[signals] : [];
+
+ propertiesArray.forEach(pspec => _checkAccessors(klass.prototype, pspec, GObject));
+
+ // Default to the GObject-specific prototype, fallback on the JS prototype.
+ const parentPrototype = parent[Gi.gobject_prototype_symbol] ?? parent.prototype;
+
+ return {
+ gtypename,
+ gflags,
+ gobjectInterfaces,
+ propertiesArray,
+ parent,
+ gobjectSignals,
+ parentPrototype,
+ };
+}
+
+
+function registerType(klass) {
+ // Ensure the class derives from GObject.Object or
+ // GObject.Interface
+ assertDerivesFromGObject(klass, 'registerType');
+
+ const {
+ gtypename,
+ gflags,
+ gobjectInterfaces,
+ propertiesArray,
+ gobjectSignals,
+ parentPrototype,
+ } = _getTypeDefinitions(klass);
+
+ const [giPrototype, registeredType] = Gi.register_type_with_class(klass, parentPrototype, gtypename,
gflags,
+ gobjectInterfaces, propertiesArray);
+
+ const config = {
+ enumerable: false,
+ writable: false,
+ configurable: false,
+ };
+
+ /**
+ * class Example {
+ * // The JS object for this class' ObjectPrototype
+ * static [Gi.gobject_prototype_symbol] = ...
+ * static [Gi.gobject_type_symbol] = ...
+ * static get $gtype () {
+ * return this[Gi.gobject_type_symbol];
+ * }
+ * }
+ *
+ * // Equal to the same property on the constructor
+ * Example.prototype[Gi.gobject_prototype_symbol] = ...
+ */
+
+ Object.defineProperties(klass, {
+ [Gi.gobject_prototype_symbol]: {
+ ...config,
+ value: giPrototype,
+ },
+ [Gi.gobject_type_symbol]: {
+ ...config,
+ value: registeredType,
+ },
+ /**
+ * $gtype is an enumerable property,
+ * so allow template classes to explicitly
+ * define it.
+ *
+ * class X extends GObject.Object {
+ * static $gtype = GObject.registerType(X);
+ * }
+ *
+ * is identical to...
+ *
+ * class Y extends GObject.Object {
+ *
+ * }
+ *
+ * GObject.registerType(Y);
+ *
+ * Internally, GJS depends on Gi.gobject_type_symbol which is
+ * not enumerable or configurable.
+ */
+ $gtype: {
+ enumerable: true,
+ configurable: true,
+ set() {
+ // Setting the $gtype is a no-op.
+ },
+ get() {
+ return this[Gi.gobject_type_symbol];
+ },
+ },
+ /**
+ * To allow types registered with GObject.registerClass
+ * to extend types regisered with GObject.registerType,
+ * we add a _classInit function which overrides the default
+ * GObject.registerClass behavior.
+ *
+ * NOTE: This does slightly change the behavior of
+ * GObject.registerClass in the context of GObject.registerType, if a
+ * class which extends a class registered with GObject.registerType,
+ * is passed to GObject.registerClass, GObject.registerClass will
+ * return the same class object it was passed.
+ */
+ _classInit: {
+ ...config,
+ // eslint-disable-next-line func-name-matching
+ value: function _classInit(klassConstructor) {
+ GObject.registerType(klassConstructor);
+
+ return klassConstructor;
+ },
+ },
+ });
+
+ Object.defineProperty(klass.prototype, Gi.gobject_prototype_symbol, {
+ ...config,
+ value: giPrototype,
+ });
+
+ _createSignals(klass[Gi.gobject_type_symbol], gobjectSignals);
+
+ gobjectInterfaces.forEach(iface => _copyAllDescriptors(klass.prototype, iface.prototype, ['toString']));
+ _copyAllDescriptors(klass.prototype, klass[Gi.gobject_prototype_symbol]);
+
+ klass.prototype.toString = function (...args) {
+ // Handle printing the GObject prototype information
+ if (this === klass.prototype)
+ return `${klass[Gi.gobject_prototype_symbol].toString(...args)}`;
+
+ // Handle printing instance information
+ if (this instanceof GObject.Object)
+ return `${GObject.Object.prototype.toString.call(this, ...args)}`;
+
+ // Fallback
+ return `${this}`;
+ };
+
+ _hookupVFuncs(klass.prototype, klass[Gi.gobject_prototype_symbol], klass[Gi.gobject_type_symbol]);
+
+ gobjectInterfaces.forEach(iface => {
+ _checkInterface(iface, klass.prototype);
+ });
+
+ return registeredType;
+}
+
// Some common functions between GObject.Class and GObject.Interface
function _createSignals(gtype, sigs) {
@@ -172,7 +358,9 @@ function _copyAllDescriptors(target, source, filter) {
.concat(Object.getOwnPropertySymbols(source))
.forEach(key => {
let descriptor = Object.getOwnPropertyDescriptor(source, key);
- Object.defineProperty(target, key, descriptor);
+
+ if (descriptor)
+ Object.defineProperty(target, key, descriptor);
});
}
@@ -430,19 +618,20 @@ function _init() {
};
GObject.registerClass = registerClass;
+ GObject.registerType = registerType;
GObject.Object._classInit = function (klass) {
- let gtypename = _createGTypeName(klass);
- let gflags = klass.hasOwnProperty(GTypeFlags) ? klass[GTypeFlags] : 0;
- let gobjectInterfaces = klass.hasOwnProperty(interfaces) ? klass[interfaces] : [];
- let propertiesArray = _propertiesAsArray(klass);
- let parent = Object.getPrototypeOf(klass);
- let gobjectSignals = klass.hasOwnProperty(signals) ? klass[signals] : [];
-
- propertiesArray.forEach(pspec => _checkAccessors(klass.prototype, pspec, GObject));
-
- let newClass = Gi.register_type(parent.prototype, gtypename, gflags,
- gobjectInterfaces, propertiesArray);
+ const {
+ gtypename,
+ gflags,
+ gobjectInterfaces,
+ propertiesArray,
+ parent,
+ gobjectSignals,
+ parentPrototype,
+ } = _getTypeDefinitions(klass);
+
+ let newClass = Gi.register_type(parentPrototype, gtypename, gflags, gobjectInterfaces,
propertiesArray);
Object.setPrototypeOf(newClass, parent);
_createSignals(newClass.$gtype, gobjectSignals);
@@ -453,29 +642,7 @@ function _init() {
['toString']));
_copyAllDescriptors(newClass.prototype, klass.prototype);
- Object.getOwnPropertyNames(newClass.prototype)
- .filter(name => name.startsWith('vfunc_') || name.startsWith('on_'))
- .forEach(name => {
- let descr = Object.getOwnPropertyDescriptor(newClass.prototype, name);
- if (typeof descr.value !== 'function')
- return;
-
- let func = newClass.prototype[name];
-
- if (name.startsWith('vfunc_')) {
- newClass.prototype[Gi.hook_up_vfunc_symbol](name.slice(6), func);
- } else if (name.startsWith('on_')) {
- let id = GObject.signal_lookup(name.slice(3).replace('_', '-'),
- newClass.$gtype);
- if (id !== 0) {
- GObject.signal_override_class_closure(id, newClass.$gtype, function (...argArray) {
- let emitter = argArray.shift();
-
- return func.apply(emitter, argArray);
- });
- }
- }
- });
+ _hookupVFuncs(newClass.prototype, newClass.prototype, newClass.$gtype);
gobjectInterfaces.forEach(iface =>
_checkInterface(iface, newClass.prototype));
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]