diff --git a/spec/gears/object_spec.lua b/spec/gears/object_spec.lua index 7c6eb57f..2a7dd5fc 100644 --- a/spec/gears/object_spec.lua +++ b/spec/gears/object_spec.lua @@ -148,7 +148,7 @@ describe("gears.object", function() assert.is.equal(obj2.foo, 42) end) - it("dynamic property disabled", function() + it("dynamic property enabled", function() local class = {} function class:get_foo() return "bar" end diff --git a/spec/gears/reactive_spec.lua b/spec/gears/reactive_spec.lua new file mode 100644 index 00000000..b76484ae --- /dev/null +++ b/spec/gears/reactive_spec.lua @@ -0,0 +1,213 @@ +--------------------------------------------------------------------------- +-- @author Emmanuel Lepage-Vallee +-- @copyright 2020 Emmanuel Lepage-Vallee <elv1313@gmail.com> +--------------------------------------------------------------------------- +_G.awesome.connect_signal = function() end + +local reactive = require("gears.reactive") +local gobject = require("gears.object") + +-- Keep track of the number of time the value changed. +local change_counter, last_counter = 0, 0 + +local function has_changed() + local ret = change_counter > last_counter + last_counter = change_counter + return ret +end + +describe("gears.reactive", function() + -- Unsupported. + if not debug.upvaluejoin then return end -- luacheck: globals debug.upvaluejoin + + local myobject1 = gobject { + enable_properties = true, + enable_auto_signals = true + } + + local myobject2 = gobject { + enable_properties = true, + enable_auto_signals = true + } + + local myobject3 = gobject { + enable_properties = true, + enable_auto_signals = true + } + + -- This will create a property with a signal, we will need that later. + myobject3.bar = "baz" + myobject1.foo = 0 + + -- Using rawset wont add a signal. It means the change isn't visible to the + -- `gears.reactive` expression. However, we still want to make sure it can + -- use the raw property even without change detection. + rawset(myobject2, "obj3", myobject3) + + -- Use a string to compare the address. We can't use `==` since + -- `gears.reactive` re-implement it to emulate the `==` of the source + -- objects. + local hash, hash2, hash3 = tostring(myobject1), tostring(myobject2), tostring(print) + + -- Make sure the proxy wrapper isn't passed to the called functions. + local function check_no_proxy(obj) + assert.is.equal(rawget(obj, "_reactive"), nil) + assert.is.equal(hash, tostring(obj)) + end + + -- With args. + function myobject1:method1(a, b, obj) + -- Make sure the proxy isn't propagated. + assert.is.equal(hash, tostring(obj)) + assert.is.equal(hash, tostring(self)) + assert.is.falsy(obj._reactive) + assert.is.falsy(self._reactive) + + -- Check the arguments. + assert.is.equal(a, 1) + assert.is.equal(b, 2) + + return myobject2, 42 + end + + -- With no args. + function myobject1:method2(a) + assert(a == nil) + assert(not self._reactive) + assert(hash == tostring(self)) + end + + -- Create some _ENV variables. `gears.reactive` cannot detect the changes, + -- at least for now. This is to test if they can be used regardless. + local i, r = 1337, nil + + it("basic creation", function() + r = reactive(function() + -- Skip busted, it uses its own debug magic which collide with + -- gears.reactive sandboxes. + local assert, tostring = rawget(_G, "assert"), rawget(_G, "tostring") + + -- Using _G directly should bypass the proxy. It least until more + -- magic is implemented to stop it. So better test it too. + local realprint = _G.print + assert(tostring(realprint) == hash3) + + -- But the "local" one should be proxy-ed to prevent the internal + -- proxy objects from leaking when calling a function outside of the + -- sandbox. + assert(tostring(print) == hash3) + + -- Make sure we got a proxy. + assert(myobject1._reactive) + + assert(not myobject1:method2()) + + local newobject, other = myobject1:method1(1,2, myobject1) + + -- Make sure the returned objects are proxied properly. + assert(type(other) == "number") + assert(other == 42) + assert(newobject._reactive) + assert(tostring(newobject) == tostring(myobject2)) + assert(tostring(newobject) == hash2) + + -- Now call an upvalue local function + check_no_proxy(myobject1) + + return { + not_object = i, + object_expression = (myobject1.foo + 42), + nested_object_tree = myobject2.obj3.bar, + original_obj = myobject1 + } + end) + + r:connect_signal("property::value", function() + change_counter = change_counter + 1 + end) + + assert.is_false(has_changed()) + + -- Make sure that the reactive proxy didn't override the original value. + -- And yes, it's actually possible and there is explicit code to avoid + -- it. + assert.is.equal(hash, tostring(myobject1)) + end) + + it("basic_changes", function() + local val = r.value + + -- The delayed magic should be transparent. It will never work + -- in the unit test, but it should not cause any visible behavior + -- change. It would not be magic if it was. + assert(val) + + -- Disable delayed. + r._private.value = nil + r._private.evaluated = false + assert.is_true(r.delayed) + r.delayed = false + assert.is.falsy(r.delayed) + + val = r.value + assert(val) + + -- Make sure the proxy didn't leak into the return value + assert.is.falsy(rawget(val, "_reactive")) + assert.is.falsy(rawget(val.original_obj, "_reactive")) + + assert.is_true(has_changed()) + + assert.is.equal(r._private.value.object_expression, 42) + assert.is.equal(r._private.value.not_object, 1337) + + myobject1.foo = 1 + + assert.is_true(has_changed()) + + assert.is.equal(r._private.value.object_expression, 43) + + -- Known limitation. + i = 1338 + assert.is.equal(r._private.value.not_object, 1337) + r:refresh() + assert.is.equal(r._private.value.not_object, 1338) + + -- Ensure that nested (and raw-setted) object property changes + -- are detected. + assert.is.equal(r._private.value.nested_object_tree, "baz") + myobject3.bar = "bazz" + assert.is_true(has_changed()) + assert.is.equal(r._private.value.nested_object_tree, "bazz") + end) + + -- gears.reactive play with the metatable operators a lot. + -- Make sure one of them work. + it("test tostring", function() + local myobject4 = gobject { + enable_properties = true, + enable_auto_signals = true + } + + local mt = getmetatable(myobject4) + + mt.__tostring = function() return "lol" end + + local react = reactive(function() + _G.assert(myobject4._reactive) + _G.assert(tostring(myobject4) == "lol") + + return tostring(myobject4) + end) + + local val = react.value + + assert.is.equal(val, "lol") + end) + + it("test disconnect", function() + r:disconnect() + end) +end) + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/tests/examples/wibox/decl_doc/reactive_expr1.lua b/tests/examples/wibox/decl_doc/reactive_expr1.lua new file mode 100644 index 00000000..e5ebf2ba --- /dev/null +++ b/tests/examples/wibox/decl_doc/reactive_expr1.lua @@ -0,0 +1,48 @@ +--DOC_GEN_IMAGE --DOC_HIDE --DOC_NO_USAGE --DOC_NO_DASH +local parent = ... --DOC_HIDE +local gears = { --DOC_HIDE + object = require("gears.object"), --DOC_HIDE + reactive = require("gears.reactive") --DOC_HIDE +} --DOC_HIDE +local wibox = require("wibox") --DOC_HIDE + + -- It's important to set 'enable_auto_signals' to `true` or it wont work. + -- + -- Note that most AwesomeWM objects (and most modules) objects can be + -- used directly as long as they implement the signal `property::` spec. + -- + -- So you don't *need* a hub object, but it's safer to use one. + local my_hub = gears.object { + enable_properties = true, + enable_auto_signals = true + } + + --DOC_NEWLINE + + -- Better set a default value to avoid weirdness. + my_hub.some_property = 42 + + --DOC_NEWLINE + + -- This is an example, in practice do this in your + -- wibar widget declaration tree. + local w = wibox.widget { + markup = gears.reactive(function() + -- Each time `my_hub.some_property` changes, this will be + -- re-interpreted. + return '' .. (my_hub.some_property / 100) .. '' + end), + widget = wibox.widget.textbox + } + + --DOC_NEWLINE + + -- This will update the widget text to '13.37' + my_hub.some_property = 1337 + +require("gears.timer").run_delayed_calls_now() --DOC_HIDE +require("gears.timer").run_delayed_calls_now() --DOC_HIDE +require("gears.timer").run_delayed_calls_now() --DOC_HIDE +assert(w) --DOC_HIDE +assert(w.markup == "13.37") --DOC_HIDE +parent:add(w) --DOC_HIDE diff --git a/tests/examples/wibox/decl_doc/reactive_expr2.lua b/tests/examples/wibox/decl_doc/reactive_expr2.lua new file mode 100644 index 00000000..ed1cf5dc --- /dev/null +++ b/tests/examples/wibox/decl_doc/reactive_expr2.lua @@ -0,0 +1,40 @@ +--DOC_GEN_IMAGE --DOC_HIDE --DOC_NO_USAGE --DOC_NO_DASH +local parent = ... --DOC_HIDE +local gears = { --DOC_HIDE + object = require("gears.object"), --DOC_HIDE + reactive = require("gears.reactive") --DOC_HIDE +} --DOC_HIDE +local wibox = require("wibox") --DOC_HIDE + +local my_hub = gears.object {--DOC_HIDE + enable_properties = true,--DOC_HIDE + enable_auto_signals = true--DOC_HIDE +} --DOC_HIDE + +my_hub.some_property = 42 --DOC_HIDE + + -- For some larger function, it's a good idea to move them out of + -- the declarative construct for maintainability. + local my_reactive_object = gears.reactive(function() + if my_hub.some_property > 1000 then + return "The world is fine" + else + return "The world is on fire" + end + end) + + --DOC_NEWLINE + + local w = wibox.widget { + markup = my_reactive_object, + forced_height = 20, --DOC_HIDE + widget = wibox.widget.textbox + } + +require("gears.timer").run_delayed_calls_now() --DOC_HIDE +require("gears.timer").run_delayed_calls_now() --DOC_HIDE +require("gears.timer").run_delayed_calls_now() --DOC_HIDE +assert(w) --DOC_HIDE +assert(w.markup == "The world is on fire") --DOC_HIDE + +parent:add(w) --DOC_HIDE diff --git a/tests/examples/wibox/widget/declarative/reactive.lua b/tests/examples/wibox/widget/declarative/reactive.lua new file mode 100644 index 00000000..5dd49c3f --- /dev/null +++ b/tests/examples/wibox/widget/declarative/reactive.lua @@ -0,0 +1,48 @@ +--DOC_GEN_IMAGE --DOC_HIDE --DOC_NO_USAGE +local parent = ... --DOC_HIDE +local gears = { --DOC_HIDE + object = require("gears.object"), --DOC_HIDE + reactive = require("gears.reactive") --DOC_HIDE +} --DOC_HIDE +local wibox = require("wibox") --DOC_HIDE + + -- It's important to set 'enable_auto_signals' to `true` or it wont work. + -- + -- Note that most AwesomeWM objects (and most modules) objects can be + -- used directly as long as they implement the signal `property::` spec. + -- + -- So you don't *need* a hub object, but it's safer to use one. + local my_hub = gears.object { + enable_properties = true, + enable_auto_signals = true + } + + --DOC_NEWLINE + + -- Better set a default value to avoid weirdness. + my_hub.some_property = 42 + + --DOC_NEWLINE + + -- This is an example, in practice do this in your + -- wibar widget declaration tree. + local w = wibox.widget { + markup = gears.reactive(function() + -- Each time `my_hub.some_property` changes, this will be + -- re-interpreted. + return '' .. (my_hub.some_property / 100) .. '' + end), + widget = wibox.widget.textbox + } + + --DOC_NEWLINE + + -- This will update the widget text to '13.37' + my_hub.some_property = 1337 + +require("gears.timer").run_delayed_calls_now() --DOC_HIDE +require("gears.timer").run_delayed_calls_now() --DOC_HIDE +require("gears.timer").run_delayed_calls_now() --DOC_HIDE +assert(w) --DOC_HIDE +assert(w.markup == "13.37") --DOC_HIDE +parent:add(w) --DOC_HIDE